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
Pattern
Doctrine relationsWhich principles this pattern expresses in code, and which its default form breaks.Library
Browse examplesReopen the wider library to compare adjacent patterns and linked learning paths.Interaction
Run sandbox nowTry the interaction directly in this example’s guided sandbox surface.Source
Open full sourceRead the real implementation, highlighted checkpoints, and runtime requirements.MCP
Call via MCPUse the same resource inside agents, deterministic exports, and MCP setup flows.
Linked principles
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.
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
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:runP6 · 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:runP8 · 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.
supervisor_workers.py
"""
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)
Related principles
- P2visibilityEnsure that background work remains perceptibleWhen the system is operating asynchronously or outside the user’s immediate focus, it should provide persistent and proportionate signals that work is continuing.Open principle →
- P6visibilityExpose meaningful operational state, not internal complexityPresent the state of the system in language and structures that are relevant to the user, rather than exposing low-level internals that do not support action or understanding.Open principle →
- P8trustMake hand-offs, approvals, and blockers explicitWhen the system cannot proceed, the reason should be immediately visible, along with any action required from the user or another dependency.Open principle →
- P9orchestrationRepresent delegated work as a system, not merely as a conversationWhere work involves multiple steps, agents, dependencies, or concurrent activities, it should be represented as a structured system rather than solely as a message stream.Open principle →