Two searches can run independently, but the answer depends on their combined evidence. What should happen if one fails? Choose orchestration around those dependencies and preserve partial failure at the join.
Before you begin: Understand agents, tool results, shared state, and task budgets.
Orchestration is the control of who runs next, what state they receive, and what counts as completion. It can be ordinary application code, a workflow graph, or a framework's team abstraction. The useful pattern follows the task's dependencies.
Start with the dependency graph
Sequential work is appropriate when one result is needed by the next step. A writer cannot ground an answer in evidence that has not been retrieved. Parallel work helps when independent searches can proceed at the same time. Routing selects one path based on the request. Review loops revisit a result after a concrete check fails.
These patterns can be combined, but every added path creates more states to handle. Start with a direct implementation and add dynamic choices only where fixed control flow cannot express the task.
| Pattern | A useful workshop example |
|---|---|
| Sequential | Retrieve records, filter eligibility, explain results. |
| Parallel | Search two independent permitted collections. |
| Routed | Send a structured date question to a deterministic parser. |
| Review loop | Revise a draft after a missing-citation check fails. |
Give every stage an output contract
A retriever returns source records and status. A filter returns eligible IDs and unresolved cases. A writer returns claims with citations. A validator returns specific failures.
Do not use “sounds finished” as a contract. Separate complete, partial, and failed results. Preserve unknown values so a later stage cannot silently turn them into confirmed facts.
The following Python 3 program illustrates a join after two independent workers. It uses only the standard library and performs no model calls.
# Runnable: Python 3, standard library.
results = [
{"worker": "public", "status": "complete", "ids": ["pottery"]},
{"worker": "members", "status": "failed", "ids": []},
]
evidence = sorted({item for result in results for item in result["ids"]})
missing = [result["worker"] for result in results if result["status"] != "complete"]
assert evidence == ["pottery"]
assert missing == ["members"]
print({"evidence": evidence, "incomplete_sources": missing})
The join preserves a failed branch instead of presenting the available record as an exhaustive search. A real application decides whether partial evidence is sufficient for the user's question.
Change the question, keep the worker results
The local join has a public result and a failed member-record search. For “Name one public workshop,” the available result may be sufficient. For “List every workshop I can attend,” the same evidence may be incomplete. Completion is relative to the user's required coverage.
Where should that decision live?
The coordinator evaluates the task contract against the collected statuses and evidence. Workers should report their own scope and outcome honestly; the writer should not infer exhaustive coverage from a nonempty list. Keep missing sources visible when producing a partial answer.
Add a late successful result from the failed branch after the coordinator has already returned. Decide whether the task can update, remains final, or requires a new attempt. Versioned artifacts and explicit final states make this behavior understandable instead of letting a background worker overwrite a completed answer silently.
Assign ownership of shared state
Let independent workers produce separate artifacts. Have one coordinator combine them using an explicit rule. Concurrently rewriting a single plan or answer can lose updates and hide disagreements.
An artifact should include its source, task ID, revision, and relevant scope. If two workers find conflicting workshop times, the coordinator applies the source-version rule or reports the conflict. It should not average the times or vote on them.
Give external side effects one owner. If both a coordinator and a worker can reserve the same seat, retries and recovery become harder to reason about.
Bound the whole workflow
A maximum of three review rounds is useful only if nested calls are also bounded. Share deadlines and usage budgets across workers. Cancel work that is no longer needed, and decide how to handle results arriving after cancellation.
Retries should match the failure. A temporary lookup outage may justify another attempt. Missing permission does not justify routing to an agent with broader access. A critic that repeats “needs improvement” without identifying a concrete defect should not cause an endless revision loop.
Practice choosing a pattern
The task compares public workshop schedules from two locations, then writes one answer. One location's service is unavailable. What should happen?
Compare your design
Run the two lookups independently, collect their statuses, and preserve the unavailable location as a gap. If the user needs a complete comparison, say that it is incomplete. If a partial answer is useful, label its coverage. Do not claim that the available location has the only suitable workshops.
Evaluate the orchestrated system against a simpler baseline using task success, evidence support, elapsed time, total usage, and recovery behavior. A more elaborate graph is useful only when it earns those costs.
The LangGraph workflows and agents guide and AutoGen team documentation describe concrete implementations of these patterns.
Continue: Production MCP servers, where tool contracts cross a service boundary.