The OpenAI Agents SDK: From Single Agent to Delegation at Scale
Learn the two core multi-agent patterns in the OpenAI Agents SDK—handoffs and agents as tools—and how to scale from a single agent to a delegating fleet of specialists.
Published on • August 20, 2026
AI Assistant

Every production chat app starts as one agent. Then someone asks for refunds, then order tracking, then FAQ answers, and suddenly one prompt is juggling three personalities, four policy sets, and a tool schema that confuses the model. The fix isn’t a bigger prompt—it’s delegation.
The OpenAI Agents SDK gives you a single mental model for this. An Agent is an LLM configured with instructions, tools, and handoffs. A Runner executes the agent loop: call the model, run tools, switch agents on handoff, repeat until a final answer. Everything else—guardrails, sessions, tracing—builds on that loop.
In this tutorial, you will learn the two core orchestration patterns, when to pick each, and how to scale from one agent to a delegating fleet.
Prerequisites
- Python 3.9+ (or a Node/TS project for the JS SDK)
pip install openai-agents- An
OPENAI_API_KEY(or any of the 100+ supported model providers)
The Core Loop
One run is one application-level turn. The runner keeps looping until it reaches a real stopping point:
- Call the current agent’s model with the prepared input.
- Inspect the output.
- If there are tool calls, execute them and continue.
- If the agent handed off to a specialist, switch agents and continue.
- If there’s a final answer with no more tool work, return.
Tools, handoffs, approvals, and streaming all build on top of this loop rather than replacing it. Understanding the loop is understanding the whole SDK.
Handoffs: Delegated Ownership
A handoff is when one agent transfers the conversation to another. The specialist becomes the active agent and owns the rest of the turn—the routing agent steps out of the way.
from agents import Agent, Runner, handoff
billing_agent = Agent(
name="Billing agent",
instructions=(
"You handle invoices, charges, and payment methods. "
"Answer briefly and clearly."
),
)
refund_agent = Agent(
name="Refund agent",
instructions=(
"You process refund requests. Gather the order id and reason, "
"verify eligibility, and confirm the refund."
),
)
triage_agent = Agent(
name="Triage agent",
instructions=(
"Route each customer request to the right specialist. "
"Hand off to billing for charges/invoices, "
"to refunds for money-back requests."
),
handoffs=[billing_agent, refund_agent],
)
result = await Runner.run(triage_agent, "I was double charged, help!")
print(result.final_output)
Under the hood, each handoff is exposed to the model as a tool named transfer_to_<agent_name>. The model picks the destination. The receiving agent sees the full conversation history and takes over.
Agents as Tools: The Manager Pattern
Sometimes the specialist should not own the final answer—the manager should. In that case, expose specialists as tools the manager can call for bounded subtasks:
from agents import Agent
summarizer = Agent(
name="Summarizer",
instructions="Summarize the given document in under 5 sentences.",
)
extractor = Agent(
name="Entity extractor",
instructions="Extract named entities from the document as JSON.",
)
manager = Agent(
name="Research manager",
instructions=(
"Analyze the user's document. Use your specialist tools to "
"summarize and extract entities, then synthesize one final report."
),
tools=[summarizer.as_tool(), extractor.as_tool()],
)
The manager keeps ownership, combines outputs from multiple specialists, and enforces shared guardrails in one place. Specialists do narrow, well-scoped work and return.
Choosing the Right Pattern
The decision rule is simple: who owns the user-facing answer?
| Pattern | How it works | Best when |
|---|---|---|
| Handoffs | Control moves to the specialist for the rest of the turn | The specialist should respond directly; routing is part of the workflow |
| Agents as tools | The manager stays in control and calls specialists | The manager synthesizes the final answer; bounded subtasks |
You can combine them. A triage agent can hand off to a specialist, and that specialist can still call other agents as tools for narrow subtasks. Add specialists only when the contract changes—splitting too early creates more prompts and more traces without improving the workflow.
Scaling: Code-Driven Orchestration
LLM-driven routing is powerful, but for predictability, speed, and cost, orchestrate in code. Common patterns:
Chaining—decompose a task into steps, transform each output into the next input:
from agents import Agent, Runner
research = Agent(name="Researcher", instructions="Research the topic and report findings.")
outliner = Agent(name="Outliner", instructions="Turn the findings into an outline.")
writer = Agent(name="Writer", instructions="Write a full blog post from the outline.")
findings = await Runner.run(research, "State of local AI models in 2026")
outline = await Runner.run(outliner, findings.final_output)
post = await Runner.run(writer, outline.final_output)
Parallelism—for independent tasks, use asyncio.gather:
import asyncio
results = await asyncio.gather(
Runner.run(summarizer, "doc1.txt"),
Runner.run(summarizer, "doc2.txt"),
Runner.run(summarizer, "doc3.txt"),
)
Evaluator loops—run a task agent, run an evaluator agent, repeat until it passes:
while True:
draft = await Runner.run(writer, "Write an API docs section")
verdict = await Runner.run(evaluator, draft.final_output)
if verdict.final_output.passes:
break
Handoff Customization: Metadata and Filtering
Handoffs can carry structured metadata and filter what the next agent sees:
from agents import handoff
escalation_handoff = handoff(
agent=escalation_agent,
input_type=EscalationReason, # model fills this in
on_handoff=log_escalation, # side effects before transfer
tool_name_override="transfer_to_escalation",
)
And you can filter conversation history so the receiving agent gets a clean view:
from agents.extensions.handoff_filters import remove_all_tools
clean_handoff = handoff(agent=billing_agent, input_filter=remove_all_tools)
Use input_type when the handoff needs a small piece of model-generated metadata like {"reason": "duplicate_charge", "priority": "high"}. Keep handoffDescription short and concrete to keep routing legible.
Putting It All Together
For a complete, runnable multi-agent routing system (triage → specialists → escalation), see the official reference scripts:
- https://github.com/openai/openai-agents-python/tree/main/examples/agent_patterns/routing.py
- https://developers.openai.com/cookbook/examples/orchestrating_agents
Conclusion & Next Steps
You now know the two patterns that carry any multi-agent system: handoffs for delegated ownership, agents-as-tools for manager-style control—and code-driven orchestration for determinism at scale.
Next steps:
- Add sessions so conversations persist across turns.
- Add input/output guardrails to screen user input and final output.
- Trace runs to see every model call, tool invocation, and handoff.
- Add a
handoff_descriptionto every specialist to keep routing legible.
Start with one agent whenever you can. Add specialists only when they materially improve capability isolation, policy isolation, or prompt clarity. That restraint is what keeps a fleet of agents manageable.