Skip to main contentSkip to footer
PatternscriptadvancedRunnableguided-flow

Supervisor and workers: route subtasks to specialists

A supervisor decomposes a request into typed subtasks, assigns each to a named specialist with its own instruction and its own slice of context, and a synthesiser merges the results while preserving disagreement.

Key Facts

Level
advanced
Runtime
Python • OpenAI API
Pattern
Inspectable flow with visible system boundaries
Interaction
Live sandbox • Script
Updated
27 July 2026

Navigate this example

High-level flow

How this example moves from input to execution and reviewable output

Supervisor and workers: route subtasks… -> Route with explicit logic -> Run the agent task -> Load environment keys -> Initialize OpenAI client -> Send chat request

Start

Supervisor and workers: route subtasks…

Checkpoint

Route with explicit logic

Outcome

Run the agent task

Why this page exists

This example is shown as both real source code and a product-facing interaction pattern so learners can connect implementation, UX, and doctrine without leaving the library.

Visual flowReal sourceSandbox or walkthroughMCP access

Doctrine relations

These are reasoning shapes: how an agent decides, when it acts, what it remembers. For how to compose the interface around it, see the AI Pattern Blueprint. For triggers, scheduling, context loading and recovery, see Agent runtime architecture.

Structural

This pattern expresses these principles directly in code.

  • P9 · Represent delegated work as a system, not merely as a conversation

    This is the pattern that makes P9 concrete. The work exists as a typed Plan of Subtasks with named owners, not as a message stream: which specialist was engaged, on what question, and what each returned are all separate addressable objects. A supervisor that passes a growing chat transcript between agents has the shape without the property.

    supervisor_workers.py:Plan

Default gap

The textbook form of this pattern violates these by default. Each one carries the fix.

  • P2 · Ensure that background work remains perceptible

    Specialists run one after another with nothing emitted between the plan and the synthesis unless the caller prints it. A multi-specialist run is the longest-latency shape in this corpus and the easiest to mistake for a hang. Fix, applied here: emit which specialist is running, and its question, before each call rather than after.

    supervisor_workers.py:run

  • P6 · Expose meaningful operational state, not internal complexity

    Every specialist's raw output is surfaced by default, so the operator sees internal chatter rather than the state of the work. At three specialists it is readable; at ten it is noise that hides which lens actually mattered. Fix: report progress as the count of specialists complete plus any disagreement detected, and keep the per-specialist text behind an inspection step.

    supervisor_workers.py:run

  • P8 · Make hand-offs, approvals, and blockers explicit

    The supervisor's plan executes as soon as it is produced. Nothing pauses between deciding to engage four specialists and spending four model calls doing it. Fix: treat the plan as a proposal with its cost stated, and gate execution on approval when the fan-out or its cost crosses a threshold you set.

    supervisor_workers.py:run

Doctrine relations are first-party analysis, not the output of a validation run. Run the validator on your own code to get a scored verdict.

How should this example be used in the platform?

Use the sandbox to understand the experience pattern first, then inspect the source to see how the product boundary, model boundary, and doctrine boundary are actually implemented.

UX pattern: Inspectable flow with visible system boundaries
Ensure that background work remains perceptible
Expose meaningful operational state, not internal complexity
Make hand-offs, approvals, and blockers explicit

Source references

Library entry
pattern-supervisor-workers
Source path
content/patterns/multi-agent/supervisor-workers/supervisor_workers.py
Libraries
openai, pydantic, python-dotenv
Runtime requirements
OPENAI_API_KEY
Related principles
Ensure that background work remains perceptible, Expose meaningful operational state, not internal complexity, Make hand-offs, approvals, and blockers explicit, Represent delegated work as a system, not merely as a conversation

supervisor_workers.py

python
"""
Supervisor and workers: route subtasks to specialists

A supervisor decomposes a request into subtasks, assigns each to a named
specialist, and a synthesiser merges the results. Each specialist runs with
its own instruction and its own slice of context.

    Request -> Plan (typed subtasks) -> Specialists -> Synthesis

The real reason to reach for this is context isolation, not intelligence:
each worker sees only what its subtask needs, so one long transcript does not
drag every step. It is not a general upgrade. Most requests are better served
by one loop, and a team of agents that does not need to be a team is just a
larger bill and more places to fail.

Reach for it when the subtasks are genuinely independent, or when they need
materially different instructions. Not when they merely sound different.

Run: python supervisor_workers.py
"""

from typing import Literal

from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field

load_dotenv()

client = OpenAI()

MODEL = "gpt-5.4-mini"
MAX_SUBTASKS = 4

#: Each specialist is an instruction, not a model. Adding one is a line here.
SPECIALISTS: dict[str, str] = {
    "technical": (
        "You assess technical feasibility and implementation risk. Name the "
        "specific mechanism that would break, not general caution."
    ),
    "financial": (
        "You assess cost and commercial impact. Give ranges and state what "
        "drives them. Never invent a figure that was not provided."
    ),
    "operational": (
        "You assess who has to run this once it exists, and what they need. "
        "Focus on the ongoing burden rather than the build."
    ),
}


class Subtask(BaseModel):
    specialist: Literal["technical", "financial", "operational"]
    question: str = Field(
        description="The single question this specialist should answer."
    )


class Plan(BaseModel):
    subtasks: list[Subtask] = Field(
        description=(
            "One subtask per specialist that is genuinely needed. Do not "
            "include a specialist whose lens adds nothing to this request."
        )
    )
    reasoning: str = Field(description="Why these specialists and not the others.")


def plan(request: str) -> Plan:
    completion = client.beta.chat.completions.parse(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Decompose the request into subtasks for the specialists "
                    "listed. Use only the specialists that genuinely add "
                    "something. Fewer is better: a specialist with nothing "
                    "useful to say costs a call and dilutes the synthesis.\n\n"
                    "Specialists:\n"
                    + "\n".join(f"- {name}: {role}" for name, role in SPECIALISTS.items())
                ),
            },
            {"role": "user", "content": request},
        ],
        response_format=Plan,
    )
    return completion.choices[0].message.parsed


def run_specialist(name: str, question: str) -> str:
    """One worker. Sees its own question, not the whole transcript."""
    completion = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": SPECIALISTS[name]},
            {"role": "user", "content": question},
        ],
    )
    return completion.choices[0].message.content or ""


def synthesise(request: str, findings: list[tuple[str, str]]) -> str:
    body = "\n\n".join(f"[{name}]\n{text}" for name, text in findings)
    completion = client.chat.completions.create(
        model=MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Merge the specialist findings into one answer. Attribute "
                    "each point to the specialist it came from. Where two "
                    "specialists disagree, say so rather than averaging them."
                ),
            },
            {"role": "user", "content": f"Request: {request}\n\n{body}"},
        ],
    )
    return completion.choices[0].message.content or ""


def run(request: str) -> str:
    print("  planning...")
    proposal = plan(request)
    print(f"      rationale: {proposal.reasoning}")

    subtasks = proposal.subtasks[:MAX_SUBTASKS]
    if not subtasks:
        # A plan with no subtasks is a real answer: this request did not need
        # a team. Say so rather than inventing work for the specialists.
        message = "No specialist lens applied to this request. Answer it directly."
        print(f"      {message}")
        return message

    findings: list[tuple[str, str]] = []
    for index, subtask in enumerate(subtasks, start=1):
        # Printed before the call, so a slow specialist reads as working.
        print(f"  [{index}/{len(subtasks)}] {subtask.specialist}: {subtask.question}")
        answer = run_specialist(subtask.specialist, subtask.question)
        print(f"      -> {answer[:80]}...")
        findings.append((subtask.specialist, answer))

    print("  synthesising...")
    result = synthesise(request, findings)
    print(f"      {result}")
    return result


if __name__ == "__main__":
    request = (
        "We are considering moving our nightly batch reconciliation to a "
        "streaming pipeline. Should we?"
    )
    print(f"\n> {request}")
    run(request)

What should the learner inspect in the code?

Look for the exact place where system scope is bounded: schema definitions, prompt framing, runtime configuration, and the call site that turns user intent into a concrete model or workflow action.

Look for output contracts and validation
Look for the exact execution call
Look for what the product could expose to the user

How does the sandbox relate to the source?

The sandbox should make the UX legible: what the user sees, what the system is deciding, and how the result becomes reviewable. The source then shows how that behavior is actually implemented.

Read the implementation summary.
Step through the user and system states.
Inspect the source code with the highlighted doctrine decisions in mind.
SandboxInspectable flow with visible system boundaries

Interaction walkthrough

Use the sandbox to step through the user-visible experience, the system work behind it, and the doctrine choice the example is making.

UX explanation

The sandbox explains what the user should see, what the system is doing, and where control or inspectability must remain explicit.

AI design explanation

The page turns raw source into a product-facing pattern: what the model is allowed to decide, what the product should expose, and where deterministic code or review should take over.

Interaction walkthrough

  1. 1Read the implementation summary.
  2. 2Step through the user and system states.
  3. 3Inspect the source code with the highlighted doctrine decisions in mind.

Visible to the user

A supervisor decomposes a request into typed subtasks, assigns each to a named specialist with its own instruction and its own slice of context, and a synthesiser merges the results while preserving disagreement.

System work

The product prepares a bounded model or workflow task.

Why it matters

The interface should make the delegated task legible before automation happens.

Used in courses and paths

This example currently stands on its own in the library, but it still connects to the principle system and the broader example family.

Related principles

Runtime architecture

Use this example in your agents

This example is also available through the blueprint’s agent-ready layer. Use the For agents page for the public MCP, deterministic exports, and Claude/Cursor setup.

Define triggers, context, and boundaries before increasing autonomy
Make control, observability, and recovery explicit in the runtime
Choose the right operational patterns before delegating to workflows