Orchestrator and Worker Patterns in Agentic Systems

How to build orchestrators that actually coordinate, workers that stay focused, and failure handling that doesn't turn one broken subtask into a total pipeline collapse.

AgentixForce Team··11 min read

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.

Orchestrator / Worker TopologyUSER TASK"research andwrite report"ORCHESTRATOR1. parse task2. create subtask plan3. delegate + wait4. aggregate resultsRESEARCH WORKERtools: web_search, read_pdf, extract_dataisolated context windowANALYSIS WORKERtools: calculate, compare, rankisolated context windowWRITING WORKERtools: draft_text, format_mdisolated context windowFINAL RESULTaggregated outputfrom all workersFig 1 — The orchestrator decomposes the task, delegates to specialized workers with isolated contexts, and aggregates their results.
The orchestrator owns planning and aggregation. Workers own execution with isolated contexts and specialized tool sets. Results flow back through dashed paths once each worker completes.

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.

python
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 completed

Delegation 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.

python
# 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 agent

The 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.

Task Lifecycle in Orchestrator / Worker PatternRECEIVEparse intentvalidate inputPLANdecomposesequence tasksDELEGATEassign workersset contextEXECUTEworkers runin parallelAGGREGATEcollect resultsformat outputworker failure → replanqueuedplanningrunningparalleldoneParallel Execution (EXECUTE stage)worker A: 340msworker B: 820msworker C: 610msFig 2 — Task lifecycle showing the PLAN → DELEGATE → EXECUTE → AGGREGATE flow, with failure triggering a replan at the PLAN stage.
The full task lifecycle from RECEIVE through AGGREGATE. Workers execute in parallel during the EXECUTE stage. A failure at EXECUTE can trigger a replan at the PLAN stage rather than a total abort.

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.

python
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 = workers

Putting 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.

More from Agentic AI

All articles
Orchestrator and Worker Patterns in Agentic Systems