What is Orchestrator-Workers?
Orchestrator-Workers is an AI design pattern for LLM apps and agents, in the Composition group. One model decides subtasks on the fly and hands them to workers.
Paula leads the migration of a legacy system, from an old framework to a new one. There are four hundred files. Some only need their imports swapped. Others have logic whose meaning changes in the new framework. And some depend on each other, and have to migrate together.
The first attempt was a single agent walking through the files. Halfway through, the context was full, and the early decisions, like how to handle the new event system, had been lost.
The second attempt was a script that sent each file to an isolated call. Fast. But files that depended on each other came out inconsistent.
What works has both parts. Someone who looks at the whole, and decides how to split it. And workers who do each part, with focus. The split changes for every module, so it can't be fixed in code. That's this lesson.
Separate three roles. The orchestrator reads the task and its context, and produces a list of subtasks, each with a self contained instruction, and dependencies. It doesn't do the work.
The workers each receive one subtask, do it in their own context, and return a result in an agreed format. Subtasks without dependencies run in parallel.
And the synthesizer combines the results, resolves conflicts, and delivers the final product. It's often the orchestrator itself, in a second call.
The difference from sectioning is who decides the split. There, your code. Here, the model, looking at the input. If you can write the loop that splits the work before you've seen the input, you don't need an orchestrator. And know the price up front. This pattern buys time and coverage with money. Teams running it at scale have reported using many times the tokens of a regular chat.
The task arrives, with the material it needs, or access to it.
The orchestrator analyzes it, and decides the split.
The plan is a structured object. Subtasks, each one's instruction, dependencies, and a done criterion.
Workers run in parallel when there's no dependency, each with its own context and budget.
Code validates each result, with a schema, tests or compilation, before moving on.
The synthesizer puts everything together, and resolves inconsistencies.
And the result goes out, with the plan and the partial results recorded on the trace.
Paula asks: migrate the payments module to the new framework. The orchestrator reads the module and returns a plan.
Four subtasks. Models, and events, with no dependencies. Then service, which depends on both. Then the API, which depends on the service. Each one with a done criterion, like the models tests pass.
Code turns those dependencies into waves. Wave one runs models and events, in parallel. Wave two runs the service, which receives both results. Wave three runs the API. Four workers, three waves, and one synthesis at the end.
And notice the conventions at the top. Events use the new event bus, never the old emit. Domain errors inherit from one payment error class. Every worker gets these.
Here's one for you. During the migration, two workers return code that compiles. But one handles errors with exceptions, and the other with return codes. The synthesis can't reconcile them. Where's the cheapest fix?
In the plan. The orchestrator fixes the error convention, and every worker receives it. Decisions that apply to several parts belong in the plan. When the convention reaches every worker before they start, the parts are compatible from the beginning.
A stronger synthesizer that rewrites one side pays to redo work that was born incoherent. And bigger workers don't help either. Each one only sees its own subtask, and can't know what the other one decided. The rule of thumb: every decision that more than one worker must make comes from the orchestrator, before any worker starts.
The split is always the same. Per file, per page, per section. Then split in code.
The task fits in one agent, with a clean context. An orchestrator adds cost, and points of failure. Start with a single loop, and split only when the context fills up.
The parts depend on each other so much that none can run alone. Parallelism disappears, and you pay for the plan and the synthesis without saving any time.
And the workers would be decorative roles, passing text along. An analyst, a reviewer and a writer, reading the same context and rewriting the same answer, split no work at all.
Quick case. Your team wants a weekly summary of thirty store reports. Each report becomes a paragraph, and a consolidated version comes out at the end. Someone proposes an orchestrator. What do you say? The split is fixed, one report per part. That's a loop in code. You get parallelism and a clean context per report, without the plan call, and without the risk of a bad plan.
And a close cousin. Plan then execute also starts with a plan. The difference is who executes. There, a single agent follows the plan step by step, in the same context. Here, each subtask goes to a worker with its own context, and independent ones run at the same time. Use the first when the steps are sequential and need what came before. Use this one when the parts fit in separate contexts, and the volume justifies parallelism.
Let's put a price on it, with illustrative numbers. The plan costs about six thousand tokens, each worker twenty thousand, and the synthesis fifteen thousand. With four subtasks, the task spends about one hundred one thousand tokens. If the orchestrator decides on twenty subtasks, the total goes past four hundred twenty thousand. And quality doesn't necessarily rise. That's why the subtask limit lives in the prompt, and also in code. The model picks the split. Code decides how much it may cost.
The failure modes. A bad plan, with overlapping subtasks, or one part missing. That spoils everything after it, so evaluate the plan on its own. For twenty real tasks, write down by hand the parts a good plan must cover. A missing part is a coverage failure. Two subtasks touching the same file is overlap. And validate the plan in code too. No cycles, no dependencies pointing nowhere.
Over-splitting. Dozens of trivial subtasks, with very short outputs and a high total cost.
And incoherence. Workers making different decisions about the same problem. You just saw the fix. Put conventions in the plan.
In production, record the plan as a span, with a child span for each worker. Put limits on subtasks and waves per task, with an alert when they hit. And keep an eye on how often each type of subtask has to be retried. A subtask type that always fails is asking for a better worker prompt.
So here's the idea to take with you. Use an orchestrator only when the split depends on the content. Let it write a plan with self contained instructions, dependencies, done criteria and shared conventions. Let code run the waves, validate each result, and cap how far the split can go.
In the next lesson, we close the agents track with a real system that uses almost everything you've seen: a coding agent. See you there.
Orquestrador e Trabalhadores
Orchestrator-Workers is an AI design pattern for LLM apps and agents, in the Composition group. One model decides subtasks on the fly and hands them to workers.
An orchestrator model decides at run time how to split the work into subtasks, hands each one to a worker, and combines the results. It is for tasks whose split cannot be predicted before looking at the input.
A site foreman who looks at the house, decides how many masons and electricians to call, and checks everything at the end.
Paula leads the migration of a legacy system from an old framework to a new one. There are 400 files. Some only need their imports swapped; others have lifecycle logic whose semantics change; some depend on each other and must migrate together.
The first attempt was a single agent walking through the files. Halfway through, the context was full and the early decisions (how to handle the new event system) had been lost. The second attempt was a script that sent each file to an isolated call. Fast, but files that depended on each other came out inconsistent.
What works has both parts: someone who looks at the whole and decides how to split it, and workers who do each part with focus. The split changes for every module, so it cannot be fixed in code.
The question this pattern answers: how do you split a large task dynamically across several models and still get a coherent final result?
Separate three roles:
The difference from Sectioning is who decides the split: there, code; here, the model, looking at the input. If you can write the for loop that splits the work before seeing the input, you do not need an orchestrator.
Anthropic’s “Building effective agents” post (December 2024) describes this pattern with two examples: coding products that change several files per request, and search tasks that gather information from several sources. In June 2025, Anthropic published how it built Claude’s research system, with a lead agent that spawns subagents in parallel. The same write-up states the cost: the multi-agent system used about 15 times more tokens than a regular chat. The pattern buys time and coverage with money.
Paula asks: “Migrate the payments module to the new framework”. The orchestrator reads the module tree and returns this plan:
{
"conventions": [
"Events use the new EventBus; never call emit() directly",
"Domain errors inherit from PaymentError"
],
"subtasks": [
{ "id": "models", "instruction": "Migrate models/*.py to the new ORM", "depends_on": [], "done_when": "models tests pass" },
{ "id": "events", "instruction": "Replace emit() with EventBus in events.py", "depends_on": [], "done_when": "events.py compiles" },
{ "id": "service", "instruction": "Migrate service.py using the new models and events", "depends_on": ["models", "events"], "done_when": "service tests pass" },
{ "id": "api", "instruction": "Update the routes in api.py", "depends_on": ["service"], "done_when": "api tests pass" }
]
}
Code turns the dependencies into waves. Wave 1 runs models and events in parallel. Wave 2 runs service, which receives both results. Wave 3 runs api. That is four workers, three waves, and one synthesis at the end.
Notice two things. Each instruction is self-contained: the events worker does not need to read the rest of the module to know what to do. And the conventions field carries the decisions that apply to the whole module to every worker.
Without conventions, each worker decides alone how to handle the event system. One uses EventBus, another keeps emit() behind an adapter, and both results compile. The synthesis gets two correct, incompatible solutions.
It is the same problem as Paula’s first attempt, only in parallel. The rule of thumb: every decision that more than one worker must make comes from the orchestrator, in the plan, before any worker starts.
flowchart TD
IN(["1. Task"]):::io --> O(["2. Orchestrator plans"]):::llm
O --> P[("3. Plan: subtasks and dependencies")]:::store
P --> W1(["4. Worker"]):::llm
P --> W2(["4. Worker"]):::llm
P --> W3(["4. Worker"]):::llm
W1 --> V["5. Validate results"]:::tool
W2 --> V
W3 --> V
V --> S(["6. Synthesize"]):::llm
S --> OUT(["7. Result"]):::io
schema Subtask { id: string, instruction: string, depends_on: list[string], done_when: string }
schema Plan { conventions: list[string], subtasks: list[Subtask] }
schema WorkResult { id: string, output: string, ok: bool, notes: string }
function orchestrate(task):
# 2 and 3. the model decides the split
plan = llm(ORCHESTRATOR_PROMPT, task) -> Plan
validate_dag(plan) # no cycles, no dangling ids
trace.span("plan", plan)
results = {}
for wave in topological_waves(plan.subtasks):
# 4. each wave runs in parallel
parallel {
for st in wave:
ctx = plan.conventions + [st.instruction] + [results[d].output for d in st.depends_on]
results[st.id] = llm(WORKER_PROMPT, ctx) -> WorkResult
}
# 5. validate before the next wave
for st in wave:
if not check(st.done_when, results[st.id]):
results[st.id] = retry_once(st, results)
budget.check()
# 6. synthesize
return llm(SYNTH_PROMPT, task, plan, results) # 7
Plan-then-Execute also starts with a plan. The difference is who executes. There, a single agent follows the plan step by step, in the same context. Here, each subtask goes to a worker with its own context, and independent ones run at the same time.
Use Plan-then-Execute when the steps are sequential and need what came before. Use Orchestrator-Workers when the parts fit in separate contexts and the volume justifies parallelism.
Suppose the plan costs 6,000 tokens, each worker 20,000, and the synthesis 15,000. With 4 subtasks, the task spends about 101,000 tokens. If the orchestrator decides on 20 subtasks, the total goes past 420,000, and quality does not necessarily rise.
That is why the subtask limit lives in the prompt and also in code. The model picks the split; code decides how much it may cost.
| Aspect | What happens | How it shows up in the trace |
|---|---|---|
| Calls | 1 plan, N workers, 1 synthesis, plus retries | plan, worker, and synthesize spans |
| Cost | Proportional to the number of subtasks the model chose | number of worker spans per task |
| Latency | Plan and synthesis in series, workers in parallel per wave | overlapping worker spans |
| Failure: bad plan | Overlapping subtasks, or one part missing | final result with a gap; no plan item matching it |
| Failure: over-splitting | Dozens of trivial subtasks | worker spans with very short output and high total cost |
| Failure: incoherence | Workers make different decisions about the same problem | conflicts resolved (or not) in synthesis |
{ "task": "Migrate the payments module to the new framework", "check": "compiles and passes the payments tests", "max_subtasks": 12 }
Evaluate the plan separately from the result. For 20 real tasks, write by hand the list of parts a good plan must cover. Then compare it with the generated plans: a missing part is a coverage failure, and two subtasks touching the same file is overlap. A bad plan spoils everything after it, so it is the first thing you fix.
plan span with the full plan, and child worker spans with each subtask and result.| Pros | Cons |
|---|---|
| The split adapts to each input | Cost is hard to predict |
| Parallelism cuts total time | The orchestrator becomes a single point of failure |
| Each worker has a clean context | Coherence across parts needs careful synthesis |
| Failed parts can be redone alone | More spans, more prompts, more to evaluate |
The tabs build the plan with structured output, run workers in parallel per wave, and synthesize. Python and TypeScript launch the waves with the language runtime. LangGraph launches each worker with Send, and state collects the returns.
Python. Use this when you want the plan validated in code and workers on asyncio.
import asyncio
import anthropic
from pydantic import BaseModel
client = anthropic.AsyncAnthropic()
class Subtask(BaseModel):
id: str
instruction: str
depends_on: list[str]
class Plan(BaseModel):
subtasks: list[Subtask]
async def plan(task: str) -> Plan: # 2 and 3
reply = await client.messages.parse(
model="claude-opus-5-5", max_tokens=8000,
system="Split the task into at most 12 self-contained subtasks. List dependencies by id.",
messages=[{"role": "user", "content": task}], output_format=Plan,
)
return reply.parsed_output
async def work(st: Subtask, deps: dict[str, str]) -> str: # 4
context = "\n\n".join(f"Result of {d}:\n{deps[d]}" for d in st.depends_on)
reply = await client.messages.create(
model="claude-sonnet-5-5", max_tokens=16000,
system="You carry out one subtask of a larger plan. Do only what was asked.",
messages=[{"role": "user", "content": f"{st.instruction}\n\n{context}"}],
)
return reply.content[-1].text
async def orchestrate(task: str) -> str:
p = await plan(task)
pending, results = {s.id: s for s in p.subtasks}, {}
while pending:
wave = [s for s in pending.values() if all(d in results for d in s.depends_on)]
if not wave:
raise ValueError("plan has a cycle or a missing dependency")
outputs = await asyncio.gather(*(work(s, results) for s in wave)) # parallel
for s, out in zip(wave, outputs):
results[s.id] = out # 5. validate here (tests, schema) before moving on
del pending[s.id]
joined = "\n\n".join(f"[{k}]\n{v}" for k, v in results.items())
final = await client.messages.create( # 6
model="claude-opus-5-5", max_tokens=16000,
system="Combine the results into one coherent final product. Point out conflicts.",
messages=[{"role": "user", "content": f"Task: {task}\n\n{joined}"}],
)
return final.content[-1].textWhat you gained: a typed plan, parallel waves, and a clear error for an invalid plan. What you lost: workers here are single calls; for work with tools, replace work with an agent loop.
TypeScript. Use this when orchestration runs on Node and you want Promise.all per wave.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const Plan = z.object({
subtasks: z.array(z.object({ id: z.string(), instruction: z.string(), depends_on: z.array(z.string()) })),
});
type Subtask = z.infer<typeof Plan>["subtasks"][number];
const text = (m: Anthropic.Message) => m.content.find((b) => b.type === "text")?.text ?? "";
async function work(st: Subtask, results: Map<string, string>): Promise<string> {
const context = st.depends_on.map((d) => `Result of ${d}:\n${results.get(d)}`).join("\n\n");
const reply = await client.messages.create({
model: "claude-sonnet-5-5", max_tokens: 16000,
system: "You carry out one subtask of a larger plan. Do only what was asked.",
messages: [{ role: "user", content: `${st.instruction}\n\n${context}` }],
});
return text(reply);
}
export async function orchestrate(task: string): Promise<string> {
const { parsed_output: plan } = await client.messages.parse({
model: "claude-opus-5-5", max_tokens: 8000,
system: "Split the task into at most 12 self-contained subtasks. List dependencies by id.",
messages: [{ role: "user", content: task }],
output_config: { format: zodOutputFormat(Plan) },
});
if (!plan) throw new Error("orchestrator returned no plan");
const results = new Map<string, string>();
let pending = plan.subtasks;
while (pending.length) {
const wave = pending.filter((s) => s.depends_on.every((d) => results.has(d)));
if (!wave.length) throw new Error("plan has a cycle or a missing dependency");
const outs = await Promise.all(wave.map((s) => work(s, results))); // 4. parallel
wave.forEach((s, i) => results.set(s.id, outs[i])); // 5. validate here
pending = pending.filter((s) => !results.has(s.id));
}
const joined = [...results].map(([k, v]) => `[${k}]\n${v}`).join("\n\n");
const final = await client.messages.create({
model: "claude-opus-5-5", max_tokens: 16000,
system: "Combine the results into one coherent final product. Point out conflicts.",
messages: [{ role: "user", content: `Task: ${task}\n\n${joined}` }],
});
return text(final);
}What you gained: Zod validates the plan and waves run in parallel. What you lost: same as Python; retries and the total budget are on you.
Python + LangGraph. Use this when each worker is a node and the parallelism has to show up on the graph, not in a gather.
import operator
from typing import Annotated
from langchain.chat_models import init_chat_model
from langgraph.graph import END, START, StateGraph
from langgraph.types import Send
from pydantic import BaseModel
from typing_extensions import TypedDict
class Task(BaseModel):
id: str
instruction: str
class Plan(BaseModel):
tasks: list[Task]
class State(TypedDict):
goal: str
plan: list[dict]
results: Annotated[list[str], operator.add]
report: str
planner = init_chat_model("anthropic:claude-opus-5-5", temperature=0).with_structured_output(Plan)
worker_model = init_chat_model("anthropic:claude-haiku-4-5", temperature=0)
synth = init_chat_model("anthropic:claude-opus-5-5", temperature=0)
def plan(state: State) -> dict:
# 2 and 3. the orchestrator only plans
drafted = planner.invoke([
("system", "Split the task into independent subtasks. Do not execute."),
("human", state["goal"]),
])
return {"plan": [t.model_dump() for t in drafted.tasks]}
def fan(state: State):
# 4. one Send per worker; they run in parallel
return [Send("work", {"goal": task["instruction"]}) for task in state["plan"]]
def work(state: State) -> dict:
reply = worker_model.invoke([("human", state["goal"])])
return {"results": [reply.text]} # operator.add merges the lists
def synthesize(state: State) -> dict:
# 5 and 6. validate in your code before this call when the criterion is a test
report = synth.invoke([("human", "\n".join(state["results"]))])
return {"report": report.text}
graph = StateGraph(State)
graph.add_node("plan", plan)
graph.add_node("work", work)
graph.add_node("synthesize", synthesize)
graph.add_edge(START, "plan")
graph.add_conditional_edges("plan", fan, ["work"])
graph.add_edge("work", "synthesize")
graph.add_edge("synthesize", END)
app = graph.compile()What you gained: each worker is a separate trace invocation, with the return reduced by operator.add. What you lost: a wave with dependencies between tasks needs a second fan after validation; this tab shows the independent wave.
| Construction | Parallelism | On this page |
|---|---|---|
asyncio or Promise.all |
The runtime | Python and TypeScript tabs |
Send |
The graph | LangGraph tab |
| One agent with many tools | The model picks the order | The wrong shape for this pattern |
Answer the 6 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
Orchestrator-Workers is an AI design pattern for LLM apps and agents, in the Composition group. One model decides subtasks on the fly and hands them to workers.
In the real-world scenarios Deep research with citations, Coding agent, and Legacy code migration.