Orchestrator/Worker Patterns with the Claude Agent SDK
An orchestrator is a top-level agent whose job is deciding what work to dispatch, waiting for or gathering results from worker subagents, and merging those results into one coherent output.
Summary
The orchestrator/worker pattern separates two concerns: dispatching and synthesizing.
The orchestrator does not do the underlying work itself. It decides which worker handles which piece, sends each worker a scoped task, and collects what comes back.
Workers can run one after another or side by side; what makes this pattern distinct is not concurrency, it's that the orchestrator owns the final merge step.
That merge step is the hard part. Two workers rarely return results in the same shape, so the orchestrator has to read both, reconcile them, and produce one output a human or downstream system can act on.
This page covers dispatching to workers with the Claude Agent SDK, and writing an orchestrator prompt that actually synthesizes worker results instead of just concatenating them.
Recipe
Quick-reference recipe card - copy-paste ready.
from claude_agent_sdk import query, AgentOptions, SubagentConfig
options = AgentOptions(
allowed_tools=["file_edit"],
subagents=[
SubagentConfig(
name="changelog-worker",
description="Summarizes code changes in a diff as a bullet list.",
allowed_tools=["bash", "file_edit"],
),
SubagentConfig(
name="risk-worker",
description="Flags risky or breaking changes in a diff, with reasoning.",
allowed_tools=["bash"],
),
],
)
async for message in query(
prompt=(
"Dispatch the changelog-worker and risk-worker on this diff, then "
"merge their results into one PR description with a Summary and a "
"Risks section."
),
options=options,
):
if message.get("type") == "text":
print(message["text"], end="", flush=True)When to reach for this:
- A task decomposes into two or more subtasks whose results need to be reconciled into one final output, not just printed side by side.
- The subtasks are heterogeneous: different workers, different tool scopes, different output shapes.
- Order matters for the merge but not necessarily for dispatch (workers can run sequentially or concurrently; the orchestrator's job starts once results are in hand).
- You want one place in your prompt or code that owns "what does the final answer look like," separate from the workers producing raw material for it.
Working Example
import asyncio
from claude_agent_sdk import query, AgentOptions, SubagentConfig
async def draft_pr_description(repo_path: str, base_branch: str) -> str:
"""Orchestrate two workers over the same diff and merge their
differently-shaped results into a single PR description."""
options = AgentOptions(
cwd=repo_path,
allowed_tools=[],
subagents=[
SubagentConfig(
name="changelog-worker",
description=(
"Reads a git diff against the base branch and returns a "
"structured bullet list of what changed, grouped by area "
"(api, ui, tests, config)."
),
allowed_tools=["bash"],
tool_config={"bash": {"allowed_commands": ["git diff", "git log"]}},
),
SubagentConfig(
name="risk-worker",
description=(
"Reads a git diff against the base branch and returns a "
"short freeform assessment of breaking changes, migration "
"steps, or rollout risk. No structure required."
),
allowed_tools=["bash"],
tool_config={"bash": {"allowed_commands": ["git diff", "git log"]}},
),
],
)
prompt = (
f"Compare the current branch against '{base_branch}'. Dispatch the "
"changelog-worker for a structured list of changes and the "
"risk-worker for a freeform risk assessment. Wait for both, then "
"merge them into one PR description with exactly two sections: "
"'## Summary' (from the changelog worker's bullets, lightly edited "
"for prose flow) and '## Risks' (from the risk worker's assessment, "
"condensed to at most 3 sentences). Do not just concatenate the two "
"raw results; write them as one cohesive document."
)
final_text = ""
async for message in query(prompt=prompt, options=options):
if message.get("type") == "text":
final_text += message["text"]
elif message.get("type") == "subagent_result":
print(f"[{message['subagent_name']} returned]")
return final_text
pr_description = asyncio.run(draft_pr_description("/repo", "main"))
print(pr_description)What this demonstrates:
- Two workers with different tool scopes, different jobs, and deliberately different output shapes: one structured (grouped bullets), one freeform (prose risk assessment).
- The orchestrator itself needs no tools (
allowed_tools=[]); its entire job is dispatching and writing the merged result. - The merge instruction is explicit in the prompt: exact target sections, and an explicit "do not just concatenate" instruction, because a model left to its own devices will often just paste both results back to back.
- Collecting the final merged text from the orchestrator's own
"text"messages, while using"subagent_result"messages only as progress signals, not as the thing you return.
Deep Dive
How It Works
- The orchestrator is the top-level agent passed to
query(); workers are itssubagents, invoked the same way any tool is invoked, from the orchestrator's own decide-act-observe loop. - Each worker runs its own internal loop to completion and returns one final result to the orchestrator; the orchestrator never sees a worker's intermediate tool calls, only its answer.
- Dispatch order is the orchestrator's call, driven by the prompt and the model's own reasoning: independent workers can be invoked back to back for concurrency, or one after another when a later worker's task depends on an earlier one's result.
- The merge happens inside the orchestrator's own final reasoning turn, after it has both workers' results in context. There's no separate "merge step" in the SDK; it's ordinary text generation, so the quality of the merge is entirely a function of how clearly your prompt specifies what the merged output should look like.
- Streamed
"text"messages after both"subagent_result"messages have arrived are the orchestrator synthesizing; that's the output your application should treat as the answer.
Sequential vs. Parallel Dispatch
| Dispatch style | When | Trade-off |
|---|---|---|
| Sequential (worker B needs worker A's output) | Worker B's task depends on worker A's result | Simpler to reason about, slower wall-clock time |
| Parallel (independent workers) | Workers operate on the same input but don't need each other's output | Faster wall-clock time, but the orchestrator still merges only after all have returned |
| Fan-out, single merge (this page's focus) | Several workers, one final synthesized output | The merge instruction matters more than the dispatch order |
Python Notes
# Give the orchestrator no tools of its own when its job is purely
# dispatch-and-merge - that keeps its role unambiguous in the prompt
# and prevents it from doing the workers' jobs itself.
options = AgentOptions(
allowed_tools=[],
subagents=[changelog_worker, risk_worker],
)
# Distinguish worker progress from the merged answer while streaming.
async for message in query(prompt=prompt, options=options):
msg_type = message.get("type")
if msg_type == "subagent_result":
continue # raw worker output, not the final merged answer
if msg_type == "text":
merged_output_chunk = message["text"]Parameters & Return Values
| Parameter | Type | Description |
|---|---|---|
subagents | list[SubagentConfig] | The workers the orchestrator can dispatch to |
allowed_tools | list[str] | Tool scope for the orchestrator itself, separate from any worker's scope |
SubagentConfig.name | str | Identifier the orchestrator uses to invoke a given worker |
SubagentConfig.description | str | Tells the orchestrator's model when this worker applies |
SubagentConfig.allowed_tools | list[str] | Tool scope for that worker only |
Gotchas
- Letting the model concatenate instead of merge. Without an explicit instruction, an orchestrator will often paste each worker's raw result back to back under separate headings instead of synthesizing them. Fix: state the target output structure and explicitly say the results must be combined, not concatenated.
- Giving the orchestrator the union of all workers' tools. This blurs who is responsible for what and lets the orchestrator skip dispatching and just do the work itself. Fix: scope the orchestrator narrowly, often to no tools at all when its only job is merging text.
- Treating
subagent_resultmessages as the final answer. These are per-worker results; your application logic needs to keep reading until the orchestrator's own"text"messages, which contain the synthesized output. Fix: accumulate"text"messages after dispatch as the answer, and use"subagent_result"only for progress logging. - Forcing parallel dispatch when a worker depends on another's output. Dispatching both workers at once when worker B actually needs worker A's result produces a worker B that's guessing. Fix: sequence dependent workers in the prompt, and only invoke independent workers concurrently.
- No cap on how many workers the orchestrator can dispatch. An open-ended prompt ("use whatever workers you need") can spiral into far more dispatches, and far more token spend, than the task warrants. Fix: name the specific workers to use in the prompt, or cap subagent count and depth in your application logic.
- Vague merge instructions leaving output shape to chance. "Combine the results" without specifying sections or format produces inconsistent output across runs. Fix: specify the exact target structure (section headings, ordering, length limits) the same way you'd specify a function's return type.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| A single agent doing everything itself | The task doesn't actually decompose into independent pieces | Two or more genuinely separable subtasks need different tool scopes or would otherwise crowd one context |
| Plain subagent delegation without a merge step | You just need each worker's raw result, reported separately | You need one coherent final output synthesized from multiple results |
| Prompt chaining (each step's output feeds the next) | Steps are strictly sequential and each only needs the previous step's output | Multiple independent workers need to be reconciled together, not just passed forward one at a time |
| Manual merge in application code (not the model) | The merge is deterministic and rule-based, e.g. concatenating two JSON blobs | The merge requires judgment, such as prose synthesis or resolving conflicting worker findings |
FAQs
What does "orchestrator" mean in this pattern, exactly?
The top-level agent in a query() call that has one or more subagents configured. It decides what to dispatch, when, and is responsible for producing the final output after workers return.
Do workers have to run in parallel for this to be an orchestrator/worker pattern?
No. What defines the pattern is that the orchestrator dispatches to workers and owns the merge step, not the concurrency. Sequential dispatch, where one worker's result is used to inform or feed the next, is equally valid.
How is this different from just delegating to subagents?
Delegation is the mechanism; orchestrator/worker is a role split built on top of it, where the orchestrator's own job is explicitly framed as dispatch-plus-merge rather than doing task work itself.
Should the orchestrator have its own tools?
Only if it needs to do something beyond dispatching and merging, such as writing the final result to a file. When its job is purely synthesis, an empty allowed_tools list keeps its role unambiguous.
How do I get the merged output out of the message stream?
Accumulate the orchestrator's "text" messages. "subagent_result" messages carry each worker's raw result and are useful for logging progress, but the synthesized answer comes from the orchestrator's own text generation after it has both results.
What if the model just concatenates the workers' results instead of merging them?
This is the most common failure mode. State the exact target structure in the prompt (section names, ordering, length limits) and explicitly instruct against concatenation; vague instructions like "combine these" default to pasting results back to back.
Can one worker's output feed into another worker's input?
Yes, that's sequential dispatch. Instruct the orchestrator to dispatch the first worker, use its result to construct the prompt for the second, and only then merge. The SDK doesn't require workers to be independent, only that you sequence dependent ones correctly.
How many workers can an orchestrator dispatch?
As many as subagents defines, but more workers means more dispatch decisions, more token spend, and a harder merge. Name the specific workers needed in the prompt rather than leaving dispatch count open-ended.
Does the orchestrator see each worker's intermediate reasoning?
No. Each worker runs its own internal loop and returns only a final result; the orchestrator's context stays clean of the workers' exploratory steps.
What guardrails matter specifically for orchestrator/worker setups?
The same three as any multi-agent setup: cap subagent depth (avoid workers spawning their own workers unless truly needed), scope each worker's tool access to least privilege for its job, and cap total token spend across dispatch plus merge, since a synthesis step reads every worker's full result into context.
Is the merge step itself a tool call?
No. Dispatching to a worker is a tool call; merging is ordinary text generation by the orchestrator once it has all the results it needs in context. There's no separate SDK primitive for merging.
When is a single agent better than an orchestrator with workers?
When the task doesn't actually decompose into independent or differently-scoped pieces. A single tool-calling loop is sufficient for most tasks; reach for orchestrator/worker only when subtasks are genuinely separable or need different tool access.
Related
- Single-Agent Loops vs Multi-Agent Systems - when this pattern is worth the added complexity
- Agentic Orchestration Basics - foundational orchestration concepts this pattern builds on
- Building Subagents for Parallel Research and Delegation - the parallel fan-out counterpart to this page's merge focus
- Guardrails for Multi-Agent Systems - capping subagent depth, tool access, and token spend
- Delegating Work to Subagents in the Claude Agent SDK - the underlying subagent mechanism this pattern is built on
- The Claude Agent SDK Mental Model - how the tool-use loop underlies both orchestrator and worker
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the Claude Agent SDK (latest release). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.