One agent rarely does everything well. A billing question, a bug report, and a refund each want a different specialist — and cramming all of that into one prompt makes an agent that is mediocre at all three. OpenAI Agents SDK handoffs solve this by letting a router transfer the conversation to the right specialist mid-turn.
This guide picks up where the OpenAI Agents SDK tutorial left off and goes deep on handoffs. You build a triage router, customize a handoff with a callback, pass structured data at the moment of transfer, control what history the specialist sees, and learn when a handoff is the wrong tool. Every snippet runs on the current SDK.
- Done the basics? This builds on the OpenAI Agents SDK tutorial
- Comfortable with
async/await— see the async primer - An OpenAI API key set in your environment — see the API key primer
- A handoff transfers control: the router hands the turn to a specialist, and the specialist produces the final answer.
- The basic form is one line — list specialist agents in
handoffs=[...]and the model routes to them. handoff()unlocks the good parts: a callback (on_handoff), a renamed tool, model-supplied data viainput_type, and history control viainput_filter.- Handoff ≠ agent-as-tool. A handoff gives up control;
as_toolkeeps it. Pick by who should own the reply.
What OpenAI Agents SDK handoffs actually do#
A handoff lets one agent delegate to another. Under the hood, the SDK exposes each handoff to the model as a tool named transfer_to_<agent_name>. So a handoff to a “Refund Agent” appears to the model as a transfer_to_refund_agent tool it can call.
That detail matters. The model chooses a handoff the same way it chooses any tool — from the description — so routing quality lives or dies on how you describe each specialist. Get the descriptions right and the router just works. For the wider picture of multi-agent design, see agent orchestration patterns.
The basic handoff#
Start simple. Give a triage agent a list of specialists in handoffs, and set each specialist’s handoff_description so the router knows when to pick it.
import asyncio
from agents import Agent, Runner
billing_agent = Agent(
name="Billing Agent",
handoff_description="Handles billing, invoices, and charges.",
instructions="Resolve billing questions clearly.",
)
refund_agent = Agent(
name="Refund Agent",
handoff_description="Processes refund requests and timelines.",
instructions="Handle refunds and explain the timeline.",
)
triage_agent = Agent(
name="Triage",
instructions="Route each customer message to the right specialist.",
handoffs=[billing_agent, refund_agent],
)
async def main():
result = await Runner.run(triage_agent, "I want a refund for a double charge.")
print(result.final_output)
print("Handled by:", result.last_agent.name)
if __name__ == "__main__":
asyncio.run(main())The router reads the message, calls transfer_to_refund_agent, and the refund agent writes the reply. result.last_agent.name tells you which specialist actually answered — log it.
Customizing with handoff()#
Passing a bare Agent works, but the handoff() function gives you control. You can rename the tool, add a callback that fires the instant a transfer happens, and override the description.
from agents import Agent, handoff, RunContextWrapper
refund_agent = Agent(name="Refund Agent", instructions="Handle refunds.")
def on_refund_handoff(ctx: RunContextWrapper[None]):
print("Routing to refunds — logging the transfer.")
triage_agent = Agent(
name="Triage",
instructions="Route billing and refund questions.",
handoffs=[
handoff(
refund_agent,
on_handoff=on_refund_handoff,
tool_name_override="escalate_to_refunds",
)
],
)The on_handoff callback is the hook for side effects — log the transfer, warm a cache, or notify a queue. It fires the moment the model decides to hand off, before the specialist runs, which is the right place for setup work.
Passing data at handoff time with input_type#
Sometimes you want the model to attach a little structured data to the transfer — a reason, a priority, a language. Set input_type to a Pydantic model and the SDK validates that payload, then passes it to your callback.
from pydantic import BaseModel
from agents import Agent, handoff, RunContextWrapper
class EscalationData(BaseModel):
reason: str
priority: str
async def on_escalate(ctx: RunContextWrapper[None], data: EscalationData):
print(f"Escalated: {data.reason} (priority={data.priority})")
escalation_agent = Agent(name="Escalation Agent", instructions="Handle escalations.")
escalation_handoff = handoff(
escalation_agent,
on_handoff=on_escalate,
input_type=EscalationData,
)Now the model must supply a reason and priority when it escalates, and you can log or persist them before the specialist takes over. Use input_type for small model-decided metadata — not for app state you already have, which belongs in the run context. New to Pydantic models? The BaseModel primer is a quick read.
Controlling what the specialist sees#
By default, the receiving agent inherits the entire conversation history. That is usually what you want — but not always. An input_filter rewrites the history before the specialist sees it. The SDK ships common filters, including one that strips all prior tool calls.
from agents import Agent, handoff
from agents.extensions import handoff_filters
faq_agent = Agent(name="FAQ Agent", instructions="Answer common questions.")
faq_handoff = handoff(
faq_agent,
input_filter=handoff_filters.remove_all_tools, # drop earlier tool calls
)A cleaner history means fewer tokens and less chance the specialist gets distracted by irrelevant tool chatter. When a specialist only needs the question, not the machinery that got you there, filter the history.
Help the model route reliably#
Handoffs work better when the model is told they exist. The SDK provides a recommended instructions prefix that primes the model to use handoffs correctly. Add it to your agents’ instructions.
from agents import Agent
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
billing_agent = Agent(
name="Billing Agent",
instructions=f"{RECOMMENDED_PROMPT_PREFIX}\nYou resolve billing questions.",
)It is a one-line change that noticeably improves routing. If your triage agent hesitates or answers instead of transferring, this prefix is the first fix to try.
Handoffs vs agents-as-tools#
This is the choice people get wrong most often. Both let agents collaborate, but they hand control very differently.
| Handoff | Agent as tool (as_tool) | |
|---|---|---|
| Who owns the reply | The specialist takes over | The orchestrator stays in control |
| The other agent | Produces the final answer | Returns a result to the caller |
| Tool name | transfer_to_<agent> | Your chosen tool name |
| Use when | Distinct domains own the response | You need a sub-answer, then continue |
Reach for a handoff when a specialist should own the outcome; reach for as_tool when you need its answer but the orchestrator should keep driving. For a broader menu of frameworks and patterns, see the best AI agent frameworks in 2026 and OpenAI Agents SDK vs LangGraph.
Common mistakes#
- Vague
handoff_descriptions. The model routes on them. “Handles stuff” earns misroutes; name the domain precisely. - Skipping
RECOMMENDED_PROMPT_PREFIX. Without it, agents sometimes answer instead of transferring. - Assuming the specialist starts fresh. It sees the full history unless you set an
input_filter. - Overloading
input_type. It is for model-generated metadata, not for dependencies you already hold in context. - Expecting one handoff to dispatch to many agents. Register one handoff per destination and let the model choose.
- Forgetting guardrail scope. Input guardrails apply only to the first agent; output guardrails only to the agent that produces the final answer.
Summary#
Handoffs turn a single overloaded agent into a team. The pattern scales from one line — a list of specialists — to a fully customized transfer with callbacks, validated data, and filtered history. Keep the descriptions sharp, add the recommended prefix, and choose handoff versus as_tool by who should own the reply. That is most of what you need to route a real multi-agent app with OpenAI Agents SDK handoffs. Before production, wrap it in evals so a routing regression shows up in a test, not a ticket.
- Need the fundamentals? Start with the OpenAI Agents SDK tutorial.
- Choosing a framework? Read OpenAI Agents SDK vs LangGraph.
- New to agents? See What Are AI Agents?.
Built a triage router with this? Tell me how many specialists you’re routing between, and I’ll flag where handoffs tend to misfire at that size.

