Orchestrator-Workers Pattern in Python: A Claude Tutorial (2026)
Build the orchestrator-workers agent pattern in Python with Claude: a planner that decides subtasks at runtime, parallel workers, and a synthesizer. Runnable code, July 2026.

On this page
Quick Answer
The orchestrator-workers pattern is an agentic workflow where one central LLM (the orchestrator) reads a task, dynamically decides how to break it into subtasks at runtime, dispatches each subtask to a worker LLM, and then a synthesis step merges the worker outputs into one answer. It differs from routing (which picks one handler) and from parallelization (which fans out a fixed, known set of subtasks) because the orchestrator does not know the number or shape of the subtasks until it sees the input. This tutorial builds a runnable version in Python with the Claude SDK, runs the workers in parallel with asyncio, and shows the cost and failure trade-offs as of July 2026.
We priced and tested this on two models: Anthropic's Claude via the
Python SDK.
What the orchestrator-workers pattern actually is
Anthropic's Building Effective Agents (December 2024) names five composable workflow patterns: prompt chaining, routing, parallelization, orchestrator-workers, and evaluator-optimizer. Four of them are static: you, the engineer, decide the control flow ahead of time. Orchestrator-workers is the one where an LLM decides the control flow at runtime.
The shape is three roles:
- Orchestrator. Reads the task and emits a plan: a list of subtasks, each with its own instruction. The plan is data, not prose. You do not know how many subtasks it will produce.
- Workers. Each runs one subtask independently. Because they do not depend on each other, they run in parallel.
- Synthesizer. Reads all worker outputs plus the original task and writes the final answer.
The canonical example, from Anthropic's Claude Cookbook, is "make this code change across a repository." The orchestrator cannot know in advance which files need editing, so it inspects the request and produces one subtask per file. A fixed fan-out cannot express that. A router cannot express it either, because routing sends the whole task down exactly one branch.
When to reach for it, and when not to
Reach for orchestrator-workers when the number and nature of the subtasks depend on the input and cannot be enumerated in advance. Research over an open set of sources, multi-file edits, and "summarize whatever documents the user attached" all fit.
Do not reach for it when the subtasks are known. If you always call the same three tools, you want parallelization, which is cheaper and has no planning hop. If the task goes down exactly one of several branches, you want routing. Adding an orchestrator to a problem that a fixed graph already solves buys you a slower, more expensive, less predictable system. The orchestrator is a real cost: it is an extra model call on the critical path, and it is a single point of failure for the whole run.
Here is the distinction the pattern write-ups usually blur, stated as a table.
Scroll to see more
| Pattern | Who decides the branches | Number of subtasks | Cheapest when |
|---|---|---|---|
| Routing | An LLM classifier | Exactly 1 branch taken | Inputs fall into known categories |
| Parallelization | You, at build time | Fixed and known | The subtasks never change |
| Orchestrator-workers | An LLM, at runtime | Variable, decided per input | Subtasks depend on the input |
Assumptions
- Python 3.11 or newer.
pip install anthropic(SDK 0.40+ as of July 2026).- An
ANTHROPIC_API_KEYin your environment. - Model choices in this tutorial:
claude-sonnet-5for the orchestrator and synthesizer (both need judgment),claude-haiku-4-5for the workers (cheap, parallel, narrow). Swap per subtask difficulty.
the orchestrator returns a typed plan
The orchestrator's job is to return structured data, not paragraphs. Ask for JSON and validate it. Never dispatch a plan you have not parsed.
import json
from anthropic import Anthropic
client = Anthropic()
ORCHESTRATOR_MODEL = "claude-sonnet-5"
PLAN_PROMPT = """You are an orchestrator. Break the task into independent subtasks.
Return ONLY JSON: {"subtasks": [{"id": "s1", "instruction": "..."}]}.
Rules: each subtask must be self-contained and runnable without the others.
Produce between 1 and 6 subtasks. Do not include commentary."""
def plan(task: str) -> list[dict]:
msg = client.messages.create(
model=ORCHESTRATOR_MODEL,
max_tokens=1024,
system=PLAN_PROMPT,
messages=[{"role": "user", "content": task}],
)
raw = msg.content[0].text
data = json.loads(raw)
subtasks = data["subtasks"]
if not (1 <= len(subtasks) <= 6):
raise ValueError(f"orchestrator returned {len(subtasks)} subtasks; out of bounds")
return subtasks
Two defenses matter here. First, the bound (1 <= len <= 6) caps the blast radius: a confused orchestrator that wants to spawn forty workers is a bug, and you want it to fail loudly, not to bill you for forty calls. Second, json.loads will raise on malformed output; wrap the call in a retry if you want resilience, but never silently proceed on a plan you could not parse.
the workers run in parallel
Workers are independent by construction, so run them concurrently. The synchronous SDK would run them one after another; use AsyncAnthropic and asyncio.gather to fan out.
import asyncio
from anthropic import AsyncAnthropic
aclient = AsyncAnthropic()
WORKER_MODEL = "claude-haiku-4-5"
async def run_worker(subtask: dict) -> dict:
try:
msg = await aclient.messages.create(
model=WORKER_MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": subtask["instruction"]}],
)
return {"id": subtask["id"], "ok": True, "output": msg.content[0].text}
except Exception as e:
return {"id": subtask["id"], "ok": False, "error": str(e)}
async def run_all(subtasks: list[dict]) -> list[dict]:
return await asyncio.gather(*(run_worker(s) for s in subtasks))
Notice the worker never raises. It returns a result envelope with an ok flag. That is deliberate: with asyncio.gather, one raised exception cancels its siblings by default. Catching inside the worker means a single failed subtask degrades the run instead of killing it, and the synthesizer can decide what to do with the gap.
the synthesizer merges, and handles partial failure
The synthesizer sees the original task and every worker envelope, including the failures, and writes the final answer.
SYNTH_MODEL = "claude-sonnet-5"
def synthesize(task: str, results: list[dict]) -> str:
done = [r for r in results if r["ok"]]
failed = [r["id"] for r in results if not r["ok"]]
blocks = "\n\n".join(f"[{r['id']}]\n{r['output']}" for r in done)
note = f"\n\nNote: subtasks {failed} failed; work around the gaps." if failed else ""
msg = client.messages.create(
model=SYNTH_MODEL,
max_tokens=2048,
system="Merge the subtask results into one coherent answer for the original task. "
"If some subtasks are missing, say so honestly rather than inventing their content.",
messages=[{"role": "user", "content": f"Task: {task}\n\nResults:\n{blocks}{note}"}],
)
return msg.content[0].text
def orchestrate(task: str) -> str:
subtasks = plan(task)
results = asyncio.run(run_all(subtasks))
return synthesize(task, results)
if __name__ == "__main__":
print(orchestrate(
"Write a launch checklist for a small SaaS: cover security, billing, and support."
))
Run it and the orchestrator decides, for this input, to spawn three workers (security, billing, support), each Haiku call runs concurrently, and Sonnet stitches them together. Give it a one-dimension task and it will spawn one worker. That runtime variability is the whole point.
Making the decision reliable
Three failure modes show up in production, and each has a cheap guard.
- The orchestrator over-decomposes. It splits a trivial task into eight subtasks and you pay for eight calls plus a synthesis. Guard: the count bound in Step 1, plus a prompt instruction to prefer fewer subtasks.
- The plan is not really parallel. If subtask 2 secretly needs subtask 1's output, running them concurrently produces garbage. Guard: instruct the orchestrator that subtasks must be independent, and if they are not, this is the wrong pattern; you want prompt chaining instead.
- Cost creep. Orchestrator-workers is inherently a cost multiplier. A rough envelope: one Sonnet plan call, N Haiku worker calls, one Sonnet synthesis. Against a single Sonnet call that just does the task, you are paying two extra Sonnet hops plus N cheap ones to buy parallelism and dynamic decomposition. If wall-clock latency does not matter and the task is small, a single call is cheaper. Measure before you reach for the orchestrator.
If you want a batteries-included version with retries, checkpoints, and a visual graph, LangGraph implements this as a
Send-based dynamic fan-out; the from-scratch version above is worth building first so you understand what the framework is doing for you.
Limitations and open questions
- The orchestrator is a single point of failure. If its plan call fails or returns junk, the whole run is dead. A retry helps; a fallback to "just run the task as one call" helps more.
- Structured-output reliability is doing a lot of work here. This tutorial parses raw JSON for clarity; in production, prefer the SDK's tool-use / structured-output path so the model is constrained to your schema rather than asked politely for it.
- Cost attribution across a variable worker count is hard to forecast. Budgeting for a workflow whose fan-out is decided by a model at runtime is an open operational problem, not a solved one.
- The synthesis step can quietly paper over failed subtasks. The honesty instruction reduces this, but verifying that the final answer actually reflects every successful worker is a separate evaluation problem.
Written by
Ren OkabeRen Okabe builds and breaks agent systems, then writes down the runnable version. Principal-engineer voice, code first.
Frequently asked questions
What is the orchestrator-workers pattern?
It is an agentic workflow where a central LLM breaks a task into subtasks at runtime, dispatches each to a worker LLM, and a synthesis step merges the results. The number of subtasks is decided per input, not fixed in advance.
How is orchestrator-workers different from routing?
Routing sends the whole task down exactly one branch chosen by a classifier. Orchestrator-workers splits the task into many subtasks that run together. Routing picks one path; orchestration decomposes and recombines.
How is it different from parallelization?
Parallelization runs a fixed, known set of subtasks you defined at build time. Orchestrator-workers lets an LLM decide the number and shape of the subtasks when it sees the input, so the fan-out varies per request.
Do the workers have to run in parallel?
No, but they should. The workers are independent by construction, so running them concurrently with asyncio.gather cuts latency. If your subtasks actually depend on each other, you want prompt chaining, not this pattern.
Is orchestrator-workers expensive?
Yes, relative to a single call. You pay for the orchestrator's planning call, one call per worker, and a synthesis call. It earns its cost only when the task genuinely needs dynamic decomposition or parallel speedup. For small, fixed tasks, a single call is cheaper.
Which Claude models should I use for each role?
As of July 2026, a common split is a capable model such as claude-sonnet-5 for the orchestrator and synthesizer (both need judgment) and a cheaper model such as claude-haiku-4-5 for narrow, parallel workers. Tune per subtask difficulty.
Related tutorials
Agentic Workflow Patterns in Python (2026)
Five reusable agentic workflow patterns, built from scratch in Python with runnable code and a start-simple rule.
Agentic Workflow Routing: Build an LLM Router in Python (2026)
A code-first routing tutorial: build a runnable LLM router in Python with the Claude API, add a deterministic fallback, and route by difficulty to cut cost.
Prompt Chaining in Python: A Runnable Claude Tutorial (2026)
A code-first prompt chaining tutorial: build a gated Python pipeline with the Claude API, add pure-Python and LLM gates, and fail fast before wasting tokens.

