The orchestrator-worker pattern is the backbone of serious multi-agent systems. Get it right and you have a setup that handles genuinely complex work in parallel, with clear failure boundaries and clean separation of concerns. Get it wrong and you've built a fragile pipeline where a single worker timing out brings everything down, and debugging it means reconstructing what five different agents were thinking simultaneously.
I've debugged enough broken multi-agent systems to know where the pain lives. It's almost never in the LLM calls themselves. It's in the coordination: delegation messages that leave workers guessing, aggregation that silently drops failed subtask results, orchestrators that blur into doing worker-level work. This post covers how the pattern actually works at the implementation level.
The Pattern Explained
The orchestrator-worker pattern separates planning from execution. The orchestrator gets the high-level goal, figures out how to decompose it, assigns subtasks to workers, and synthesizes the results. Workers are intentionally narrow, each one has a specific capability, a focused toolset, and a system prompt built for one type of work. The orchestrator coordinates; it does not execute.
The isolation isn't just about parallelism, it's about cognitive clarity. A research worker that only sees its search query and the returned results doesn't have to reason around the full project context to decide what to do. Focused context produces better tool call decisions. This matters more than most people expect when you're looking at why a worker keeps making weird choices.
What the Orchestrator Actually Does
Decompose, delegate, aggregate sounds simple. Each of those verbs hides real complexity that shows up the moment you try to implement it.
Decomposition isn't just breaking a task into pieces, it's understanding which pieces have dependencies and which can run simultaneously. A good orchestrator builds a dependency graph: subtask B can't start until subtask A returns, but C and D can run in parallel with A. When A finishes, the orchestrator immediately dispatches B. This kind of dependency-aware dispatch is what turns a sequential pipeline into something that actually benefits from multiple agents.
from dataclasses import dataclass, field
from typing import Optional
import asyncio
@dataclass
class SubTask:
id: str
description: str
worker_type: str
depends_on: list[str] = field(default_factory=list)
result: Optional[str] = None
status: str = "pending" # pending | running | done | failed
class Orchestrator:
def __init__(self, workers: dict):
self.workers = workers # worker_type -> worker_fn
async def run(self, task: str) -> str:
# Step 1: Ask LLM to decompose the task into a subtask plan
subtasks = await self.plan(task)
# Step 2: Dispatch in dependency order
results = await self.dispatch(subtasks)
# Step 3: Synthesize final answer
return await self.aggregate(task, results)
async def dispatch(self, subtasks: list[SubTask]) -> dict[str, str]:
completed: dict[str, str] = {}
while len(completed) < len(subtasks):
# Find subtasks ready to run (all dependencies resolved)
ready = [
st for st in subtasks
if st.status == "pending"
and all(dep in completed for dep in st.depends_on)
]
if not ready:
break # deadlock or all done
# Run ready subtasks concurrently
results = await asyncio.gather(*[
self.run_worker(st) for st in ready
], return_exceptions=True)
for st, result in zip(ready, results):
if isinstance(result, Exception):
st.status = "failed"
# Could retry, skip, or escalate depending on policy
else:
st.status = "done"
st.result = result
completed[st.id] = result
return completedDelegation is where most orchestrators leave workers to figure things out on their own. Don't forward the raw top-level task description to each worker. Construct a focused message that tells this specific worker exactly what it needs to do, gives it only the context it actually needs, and tells it what the output should look like. Vague delegation messages produce vague worker outputs that are hell to aggregate.
Designing Workers That Are Actually Useful
Here's the thing I see constantly in poorly designed multi-agent systems: workers that are too general. The system prompt says 'You are a helpful assistant that can search the web and analyze data.' That's not a worker, that's just another general-purpose agent that happens to be called by an orchestrator. You haven't gained anything.
An effective worker is narrow by design. It has one capability. Its tool list contains only what that capability needs. It never has to decide what kind of work it should be doing, that decision belongs to the orchestrator. The worker should be able to complete its assigned subtask without knowing why it's doing the work. If it needs that context, the delegation message is incomplete.
# Well-designed worker, narrow, focused, purpose-built
RESEARCH_WORKER_PROMPT = """
You are a research specialist. Your only job is to find factual information.
Given a research query, you will:
1. Search the web for relevant sources
2. Read the most relevant sources
3. Extract the key facts that directly answer the query
4. Return a structured summary with source citations
You do NOT: write prose, make recommendations, or go beyond the query.
Output format: JSON with keys "facts" (list) and "sources" (list of URLs).
"""
async def research_worker(query: str) -> dict:
response = await client.messages.create(
model="claude-haiku-4-5-20251001", # cheaper model is fine for focused work
system=RESEARCH_WORKER_PROMPT,
messages=[{"role": "user", "content": f"Research query: {query}"}],
tools=[web_search_tool, read_url_tool], # ONLY the tools this worker needs
max_tokens=4096,
)
return parse_json_response(response)
# Poorly designed worker, too general
BAD_WORKER_PROMPT = """
You are a helpful assistant that can research, analyze, and write content.
Use your tools as needed to help with the assigned task.
"""
# This provides no real benefit over a single general-purpose agentThe Delegation Protocol
The message the orchestrator sends to a worker is the single most important design decision in the whole pattern. A vague delegation message produces vague worker output. And vague worker output is either useless for aggregation or actively misleading.
Every delegation message needs to answer four questions for the worker: what specifically is the task, what context is directly relevant (not all context, the relevant slice), what format the output should take, and what to do if it can't complete the task. That last one is critical and almost always missing. Should the worker report failure, return partial results, or return a default? The orchestrator's aggregation logic depends on knowing what shape failures come back in.
Handling Worker Failures
Worker failures aren't edge cases, in any system running dozens of parallel workers on complex tasks, some workers will fail. The orchestrator needs a defined policy for each failure mode before it ships to production. Not a vague 'we'll handle it', an actual encoded policy.
- Hard failure with retry: the worker errored out or timed out. Retry with exponential backoff up to N times. If it still fails after N retries, escalate.
- Soft failure with partial results: the worker completed but couldn't fulfill the full request. Accept what it returned, note the gap explicitly when aggregating, and let the orchestrator decide whether the partial result is usable.
- Quality failure: the worker returned results that don't meet the threshold. Requeue with additional context or route to a different worker type.
- Dependency failure: an upstream worker that a downstream worker depends on has failed. Cancel or defer all dependent workers. Decide whether to replan the affected subtrees or abort the task.
Result Aggregation Strategies
This is where most orchestrators get lazy. 'Pass all worker outputs to the LLM and ask it to combine them' sounds fine but produces inconsistent results unless you've told it precisely what combining means in your context.
Your aggregation prompt needs to specify: what the final output format looks like, how to handle conflicting information from different workers (which source wins? how to flag the conflict?), how to note gaps where workers failed or returned partial results, and what level of synthesis is expected. Collation and synthesis are different things, make sure the orchestrator knows which you want.
Where Orchestrators Go Wrong
- The orchestrator starts doing worker-level work. If your orchestrator is making direct tool calls to execute specific tasks rather than delegating them, the abstraction boundary is broken. Orchestrators coordinate; they do not execute.
- Workers with overlapping responsibilities. When two workers can both do the same type of work, the orchestrator faces ambiguous routing decisions that produce inconsistent outputs. Each worker type should own exactly one capability.
- Aggregating before all workers complete. Premature aggregation produces results with invisible holes. Always wait for all non-failed workers before passing results to aggregation.
- Delegation messages missing context. Workers that don't have what they need make worse decisions. It's more common than you'd think, the orchestrator assumes workers know things they don't.
- Orchestrator as a single point of failure. If the orchestrator crashes mid-execution, all worker results may be lost. Persist orchestrator state and worker results to durable storage at every phase boundary.
A Production Implementation
A real production orchestrator needs more than the core dispatch loop. It needs persistence for in-flight task state so a crash doesn't lose everything, structured logging of all inter-agent communication so you can reconstruct what happened, per-worker-type metrics so you can see which workers are slow or failing, and a clean interface for adding new worker types without touching the orchestrator logic.
from typing import Protocol
import json
import logging
logger = logging.getLogger(__name__)
class Worker(Protocol):
"""Every worker must implement this interface."""
worker_type: str
async def execute(self, task_description: str, context: dict) -> dict:
"""Returns {"result": ..., "status": "done"|"partial"|"failed", "notes": ...}"""
...
class ProductionOrchestrator:
def __init__(self, workers: dict[str, Worker], state_store):
self.workers = workersPutting It Together
The orchestrator-worker pattern earns its complexity when you have genuinely parallel independent subtasks, capability isolation requirements, or long-horizon tasks that exceed a single context window. It pays for itself in those situations. In simpler situations, it's overhead.
The implementation details that separate working production systems from demos are: dependency-aware dispatch, precise delegation messages, explicit failure policies, and aggregation prompts that specify exactly what 'combine' means. Get those four right and the rest follows naturally.
Frequently Asked Questions
Build secure agentic systems with AgentixForce
We help companies design, build, and secure production-grade AI agent systems. From architecture reviews to full implementation, our team brings deep expertise to every engagement.