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.
Search across all documentation pages
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.
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.
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:
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:
allowed_tools=[]); its entire job is dispatching and writing the merged result."text" messages, while using "subagent_result" messages only as progress signals, not as the thing you return.query(); workers are its subagents, invoked the same way any tool is invoked, from the orchestrator's own decide-act-observe loop."text" messages after both "subagent_result" messages have arrived are the orchestrator synthesizing; that's the output your application should treat as the answer.| 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 |
# 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"]| 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 |
subagent_result messages 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.| 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 |
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
Reviewed by Chris St. John·Last updated Jul 18, 2026