Skip to main content
Lesson 12 of 25 · Building OpenAI Agents

Handoffs: Orchestrating Multiple Agents

Lesson 12 of 25 Updated July 29, 2026 Developer
On this page
  1. Handoffs: Orchestrating Multiple Agents
  2. The Handoff Concept
  3. Customizing the Handoff Description
  4. Handoffs with Context
  5. Pattern: Three-Level Triage
  6. Bidirectional Handoffs
  7. Tracking Handoffs
  8. Key Takeaways

Handoffs: Orchestrating Multiple Agents

A single agent can’t do everything. The more domains you pile into one set of instructions, the more the guidelines contradict each other and the tepider the responses become. Handoffs let an agent delegate the conversation to another specialized agent. This is the key mechanism for building multi-agent systems where each agent excels in its own domain.

The Handoff Concept

A handoff is a transfer of control from one agent to another. When Agent A hands off to Agent B, Agent B takes over and generates the response. The Runner handles this transition automatically: you don’t write explicit routing logic or manually call the second agent. In the example below, the triage agent receives a complaint about an invoice amount, recognizes the domain, and passes the baton. The customer, meanwhile, sees only one continuous conversation.

from agents import Agent, Runner

billing_agent = Agent(
    name="Billing Agent",
    instructions="""You handle billing questions: invoices, payments,
    refunds, subscriptions. Respond precisely and professionally.""",
    model="gpt-5.6-terra",
)

technical_agent = Agent(
    name="Technical Agent",
    instructions="""You handle technical support: bugs, configuration,
    installation, troubleshooting. Provide step-by-step instructions.""",
    model="gpt-5.6-terra",
)

triage_agent = Agent(
    name="Triage Agent",
    instructions="""You are the first point of contact.
    Analyze the customer's request and transfer it to the appropriate agent.
    - Billing questions → Billing Agent
    - Technical questions → Technical Agent""",
    handoffs=[billing_agent, technical_agent],
    model="gpt-5.6-terra",
)

result = Runner.run_sync(
    triage_agent,
    "My latest invoice seems incorrect; the amount is too high."
)
# The triage agent hands off to the billing agent
print(f"Answered by: {result.last_agent.name}")
print(result.final_output)

Customizing the Handoff Description

By default, a handoff uses the target agent’s name and instructions. This works fine as long as the names are descriptive, but becomes risky when two agents look alike. In those cases, you can customize the description to guide routing by listing the specific scenarios that should trigger each transfer.

from agents import Handoff

triage_agent = Agent(
    name="Triage Agent",
    instructions="Transfer the request to the specialized agent.",
    handoffs=[
        Handoff(
            agent=billing_agent,
            description="Transfer for payment questions, invoices, refunds, and subscriptions.",
        ),
        Handoff(
            agent=technical_agent,
            description="Transfer for technical issues, bugs, and configuration help requests.",
        ),
    ],
)

Handoffs with Context

A transfer is only useful if the agent taking over knows what’s going on. When an agent performs a handoff, the conversation history is passed to the target agent: the new agent has access to everything said so far, so the customer doesn’t have to repeat their problem. On top of that, typed context carries structured session data—customer ID, subscribed plan, past tickets—and remains accessible to the incoming agent’s tools.

from dataclasses import dataclass
from agents import Agent, Runner, RunContextWrapper, function_tool

@dataclass
class CustomerContext:
    client_id: str
    plan: str
    ticket_history: list[str]

@function_tool
def view_invoice(ctx: RunContextWrapper[CustomerContext], month: str) -> str:
    """Views the invoice for a given month for the current customer."""
    client_id = ctx.context.client_id
    return f"Invoice for {month} for {client_id}: €149.99 (plan {ctx.context.plan})"

billing_agent = Agent(
    name="Billing Agent",
    instructions="You handle billing. Use the customer context.",
    tools=[view_invoice],
    model="gpt-5.6-terra",
)

triage_agent = Agent(
    name="Triage",
    instructions="Transfer to the right agent.",
    handoffs=[billing_agent],
    model="gpt-5.6-terra",
)

context = CustomerContext(
    client_id="cli_456",
    plan="Pro",
    ticket_history=["Ticket #001: Slow connection"]
)

result = Runner.run_sync(
    triage_agent,
    "Can you check my invoice for March?",
    context=context,
)

Pattern: Three-Level Triage

Handoffs aren’t limited to horizontal routing. They also replicate the escalation structure of a real support service. Here is a production pattern with three levels: L1 handles routine inquiries, transfers advanced configuration issues to L2, and L2 escalates architecture topics to L3. Note that level 3 relies on a more capable model, since it only sees cases already filtered by the two preceding tiers.

level3_agent = Agent(
    name="L3 Technical Expert",
    instructions="""You are a level 3 technical expert.
    You handle complex architecture and infrastructure problems.
    If you cannot resolve the issue, indicate that human escalation is necessary.""",
    model="gpt-5.6-sol",  # The most capable model for complex cases
)

level2_agent = Agent(
    name="L2 Technical Support",
    instructions="""You are a level 2 technician.
    You handle advanced configuration problems.
    If the problem is too complex, transfer to level 3.""",
    handoffs=[level3_agent],
    model="gpt-5.6-terra",
)

level1_agent = Agent(
    name="L1 Customer Support",
    instructions="""You are the first point of contact for customer support.
    Handle simple questions (password, account, navigation).
    Transfer technical problems to level 2.""",
    handoffs=[level2_agent],
    model="gpt-5.6-terra",
)

Bidirectional Handoffs

The chain can also be circular: two agents can transfer control back and forth. This is typical when a prospect asks a technical question during a sales conversation, then returns to pricing once reassured.

sales_agent = Agent(
    name="Sales Agent",
    instructions="""You sell products. If the customer asks
    for technical support, transfer to the technical agent.""",
)

support_agent = Agent(
    name="Support Agent",
    instructions="""You provide technical support. If the customer
    wants to buy something, transfer to the sales agent.""",
)

# Bidirectional handoffs
sales_agent.handoffs = [support_agent]
support_agent.handoffs = [sales_agent]

Watch out for infinite loops: a guardrail or max_turns is recommended. Without this limit, an ambiguous request can bounce the conversation from one agent to another until your quota is exhausted.

Tracking Handoffs

Finally, you need to know who actually answered—both to verify your routing and for your support statistics. The RunResult tells you which agent ultimately produced the response.

result = Runner.run_sync(triage_agent, "I have a critical bug")

print(f"Initial agent: triage_agent")
print(f"Final agent: {result.last_agent.name}")
# Prints "Technical Agent" if triage worked correctly

Make it a habit to log this value: if a triage agent keeps control of requests it should have transferred, the distribution of last_agent will reveal the issue well before customer feedback does.

Key Takeaways

  • Handoffs let you delegate the conversation to a specialized agent
  • Conversation history is passed to the target agent
  • Use Handoff(agent=..., description=...) to guide routing
  • Shared context (RunContextWrapper) is accessible to all agents
  • result.last_agent.name indicates which agent finally responded
  • Protect bidirectional handoffs with max_turns to prevent loops