supervisor recipe
The Supervisor pattern: routing to specialist sub-agents
A supervisor agent that reads the user message and routes to specialist sub-agents (docs, billing, escalation). The default pattern for multi-domain customer-facing agents.
5 min read · Published May 2, 2026 · Languages: python, typescript, rust, go, java
The pattern
A Supervisor reads the user message, picks a specialist sub-agent to handle it, and forwards the call. The supervisor doesn’t do the work — it routes.
Supervisor in one line: One LLM call decides which sub-agent runs, then the sub-agent runs to completion. The supervisor doesn’t think about tools — only about which specialist owns this conversation turn.
Prebuilt
from agentmatic.prebuilt import create_supervisor, create_react_agent, create_rag_agent
docs = create_rag_agent(llm=OpenAI(), vectorstore=Qdrant.from_env())
billing = create_react_agent(llm=OpenAI(), tools=[get_invoice, refund])
escalation = create_react_agent(llm=OpenAI(), tools=[send_to_human])
supervisor = create_supervisor(
llm=OpenAI("gpt-4o"),
agents={"docs": docs, "billing": billing, "escalation": escalation},
system_prompt="""
Route to docs for product questions.
Route to billing for invoice/refund questions.
Route to escalation for anything off-policy.
""",
)
What makes it work
- Hard separation — each sub-agent has its own tool set, system prompt, and memory.
- Clear routing prompt — supervisor sees the message and a one-line summary of each agent.
- Bounded recursion — supervisors don’t supervise other supervisors (we have a
max_depthguard).
Compared to handing all tools to one ReAct agent
| ReAct with all tools | Supervisor | |
|---|---|---|
| Prompt size | Grows with toolset | Constant |
| Cost per turn | Higher (more tokens) | Lower |
| Tool confusion | More common | Rare |
| Add a new specialty | Add tools, retest all | Add a sub-agent, no retest |
For 3+ distinct domains, Supervisor wins on quality, cost, and maintainability.
Custom routing (hand-rolled)
If you don’t want the LLM doing routing — e.g., you want deterministic rule-based routing — use a StateGraph with add_conditional_edges:
def route(state):
intent = classify(state.messages[-1].content)
return {"product": "docs", "billing": "billing", "other": "escalation"}[intent]
graph.add_conditional_edges(START, route, {"docs": "docs", "billing": "billing", "escalation": "escalation"})