Skip to content
Blog

Handoff Design: Passing State and Control Between Agents

A deep dive into multi-agent handoff patterns, state passing, and control flow design principles — with practical examples from the OpenAI Agents SDK.

Published on September 9, 2026

AI Assistant

Most multi-agent failures don’t happen inside an individual agent’s reasoning — they happen at the handoff. The moment one agent passes control, context, or a partial result to another is where things silently break. A single agent with a bad prompt produces a bad output you can see immediately. A broken handoff produces a system that looks like it’s working right up until the final output is subtly, confidently wrong.

This post covers the core patterns, design principles, and practical implementation details for building reliable handoffs between AI agents, with examples drawn from the OpenAI Agents SDK.

Why Handoffs Are the Hardest Part of Multi-Agent Systems

Teams building multi-agent systems tend to spend most of their debugging effort tuning individual agent prompts. In practice, the majority of production incidents trace back to what crosses the boundary between agents: incomplete context, silently dropped state, or an agent acting on a result it misinterpreted because the handoff didn’t specify a strict enough contract.

A two-agent handoff is easy to reason about manually. A five-agent pipeline with conditional branching creates handoff combinations that are effectively impossible to fully trace by hand. This is exactly why handoff design needs to be treated as a first-class architectural concern, not an implementation detail left to whatever the orchestration framework does by default.

As the Microsoft Agent Framework team puts it: a handoff is decentralized routing. The developer declares a graph of agents and the directed edges between them; the framework gives each agent a synthetic handoff tool per outbound edge so it can pass control by calling one. That single mechanic gives the authoring model three properties that matter day-to-day:

  1. The conversation is one shared transcript, not a fan-out of independent threads.
  2. The topology is enforced — an agent can only hand off to declared targets.
  3. The graph terminates naturally when the active agent finishes a turn without invoking a handoff.

Three Handoff Patterns

There is no one-size-fits-all handoff pattern. The right choice depends on the shape of your task graph.

Pattern 1: Sequential Handoff

Agent A completes fully, passes a structured result to Agent B, which starts fresh. This is the simplest pattern and works well for linear pipelines like research → draft → review.

The critical design choice is that each agent receives structured context, not raw previous output. The writer agent doesn’t get the research scratchpad — it gets a structured summary, key facts, and sources. The editor doesn’t get the research at all; it just gets the draft to edit.

Passing raw previous output bloats context and introduces noise. The research agent’s scratchpad might include hundreds of tokens of reasoning about which sources to trust. None of that is useful to the writer.

Best fit: Linear pipelines with deterministic order. Main risk: No shared context after handoff — B can’t ask A a follow-up.

Pattern 2: Shared-Context Handoff

Agents operate against a common, persistent context object or memory store rather than passing discrete messages. This pattern supports back-and-forth collaboration across multiple turns.

Best fit: Collaborative tasks needing multi-turn planning. Main risk: Context bloat and stale-state bugs if not pruned aggressively.

Pattern 3: Supervisor-Routed Handoff

A controlling agent (the supervisor or triage agent) decides which specialist agent should handle each step based on the current state. Rather than a predetermined sequence, the supervisor reads the task and routes dynamically.

This is the pattern the OpenAI Agents SDK implements with its handoffs parameter. Under the hood, the SDK automatically creates a special tool for each potential handoff. If you have an agent named “Refund Agent,” the SDK creates a tool called transfer_to_refund_agent. When the LLM decides to call this tool, the SDK intercepts it and executes the handoff protocol.

Best fit: Systems with many possible specialist agents and unpredictable task paths. Main risk: Supervisor becomes a single point of routing failure.

Most production systems end up using a mix — sequential handoffs within a fixed sub-pipeline, wrapped inside a supervisor-routed outer loop.

The Handoff Contract: What to Pass and Why

The single highest-leverage practice in multi-agent design is treating every handoff as an explicit contract — a defined schema for what crosses the boundary, not a free-form natural-language summary the next agent has to interpret.

A handoff contract should specify:

  • Required fields, not just useful ones. If the receiving agent cannot function without a specific piece of state, that field should be structurally required (schema validation failure, not a missing-context guess) rather than optional.
  • Explicit completion status, not inferred from the presence of output. An agent that fails partway through should hand off a clear “incomplete” status rather than a plausible-looking partial result that the next agent treats as finished work.
  • Provenance of the data — where did this claim, number, or decision come from? Without this, downstream agents can’t distinguish a verified fact from an earlier agent’s hallucination that’s now been passed along as established context.
  • A version or step identifier, so that when something goes wrong three agents downstream, you can trace exactly which handoff introduced the problem.

What to Pass

  • The original task or user intent (the goal, not just the previous output)
  • Key results from previous steps (structured, not raw)
  • Relevant constraints that should carry through the pipeline
  • Current task state (what’s been done, what’s left)

What Not to Pass

  • Intermediate reasoning from previous agents (unless specifically relevant)
  • Error messages that have already been handled
  • The entire conversation history when a summary would do

One practical framework from recent research on context handoff in multi-agent systems introduces the concept of decision-sufficiency: context should be transferred only insofar as it is required for the receiving agent to make the next correct local decision within its planning horizon. This is a sharper criterion than “preserve all history.”

Implementing Handoffs in the OpenAI Agents SDK

The OpenAI Agents SDK provides a clean, Python-first API for handoffs. Here’s the basic setup:

from agents import Agent, handoff

billing_agent = Agent(
    name="Billing agent",
    handoff_description="Handles billing, invoices, and charges.",
)

refund_agent = Agent(
    name="Refund agent",
    handoff_description="Processes refund requests and timelines.",
)

triage_agent = Agent(
    name="Triage agent",
    instructions="Route each customer message to the right specialist.",
    handoffs=[billing_agent, refund_agent],
)

The triage agent reads the message, decides which specialist should handle it, and calls the corresponding transfer_to_* tool. The specialist then takes over the conversation and produces the final answer.

Customizing Handoffs with handoff()

The handoff() function gives you control over the delegation process:

from agents import Agent, handoff, RunContextWrapper

def on_seat_booking_handoff(ctx: RunContextWrapper[None]):
    print("Handoff to seat booking — logging the transfer.")

seat_booking_agent = Agent(
    name="Seat Booking Agent",
    handoff_description="Can update a seat on a flight.",
)

triage_agent = Agent(
    name="Triage Agent",
    handoffs=[
        handoff(
            agent=seat_booking_agent,
            on_handoff=on_seat_booking_handoff,
            tool_name_override="transfer_to_seat_booking_agent",
        )
    ],
)

Key customization options:

  • tool_name_override: Rename the auto-generated tool (default: transfer_to_<agent_name>).
  • on_handoff: A callback that fires the instant a transfer happens, before the specialist runs. Use this for logging, cache warming, or notifications.
  • input_type: A Pydantic model describing structured metadata the LLM should supply with the handoff (e.g., reason, priority).
  • input_filter: Controls what conversation history the receiving agent sees.

Passing Structured Data at Handoff Time

Sometimes you want the model to attach structured data to the transfer — a reason, a priority, a language preference. The input_type parameter handles this:

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,
)

The SDK exposes the schema to the model as the handoff tool’s parameters, validates the returned JSON locally, and passes the parsed value to on_handoff. Use input_type for small model-decided metadata — not for application state you already hold in the run context.

Controlling What History 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:

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,
)

The SDK ships common filters, including one that strips all prior tool calls. When a specialist only needs the question, not the machinery that got you there, filter the history. This saves tokens and reduces confusion.

The SDK provides a recommended instructions prefix that primes the model to use handoffs correctly:

from agents import Agent
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX

billing_agent = Agent(
    name="Billing Agent",
    instructions=f"""{RECOMMENDED_PROMPT_PREFIX}
    You resolve billing questions.""",
)

This 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.

The Customer Service Example in Full

The official customer service example in the SDK repository demonstrates these patterns working together:

class AirlineAgentContext(BaseModel):
    passenger_name: str | None = None
    confirmation_number: str | None = None
    seat_number: str | None = None
    flight_number: str | None = None

# Shared context persists across handoffs within a run
context = AirlineAgentContext()

# Triage routes to specialists
triage_agent = Agent[AirlineAgentContext](
    name="Triage Agent",
    instructions="You are a helpful triaging agent.",
    handoffs=[
        handoff(agent=faq_agent, tool_name_override="transfer_to_faq_agent"),
        handoff(
            agent=seat_booking_agent,
            on_handoff=on_seat_booking_handoff,
            tool_name_override="transfer_to_seat_booking_agent",
        ),
    ],
)

# Specialists can hand off back to triage
faq_agent.handoffs.append(
    handoff(agent=triage_agent, tool_name_override="transfer_to_triage_agent")
)

# The conversation loop tracks current_agent across handoffs
result = await Runner.run(current_agent, input_items, context=context)
current_agent = result.last_agent  # Which agent actually answered?

The pattern is: triage routes to a specialist, the specialist handles the request, and if it encounters something outside its scope, it hands off back to triage. The AirlineAgentContext object persists across these transitions, carrying session-level state like confirmation numbers and flight information.

Common Failure Modes

Handoffs fail in specific, reproducible ways:

Context loss. The receiving agent doesn’t understand what it needs to do because the handoff didn’t include enough of the original task context. Fix: always include the original intent, not just the previous output.

Goal drift. After three or four handoffs, the task the last agent is working on bears little resemblance to the original request. Each agent slightly reinterprets the task. Fix: pass the original task explicitly to every agent in the chain.

Compounding errors. Agent A makes a mistake. Agent B receives that mistake as ground truth and builds on it. Fix: validate each agent’s output before passing it forward.

Loop detection failure. A supervisor routes to Agent A, which routes back to the supervisor, which routes to Agent A again. Without a step counter and kill condition, this runs indefinitely. Fix: implement a max_steps kill condition in every orchestrator, and log every routing decision.

Context window overflow. The cumulative context from multiple agents fills the receiving agent’s context window. Fix: summarize, don’t concatenate. Before passing context forward, compress prior agent outputs to the essential facts.

The LangChain documentation highlights another subtle issue: when a handoff uses Command.PARENT, the parent history must contain both the AIMessage that called the tool and a matching ToolMessage acknowledging the handoff. Without this pairing, the receiving model sees malformed conversation history.

When Handoffs Are the Wrong Tool

Handoffs are not always the right pattern. The key distinction is who owns the reply:

  • Handoff: The specialist takes over the turn and produces the final answer. Control is transferred.
  • Agent-as-tool: The orchestrator stays in control, calls a specialist for a bounded subtask, and continues. Control is retained.

Use handoffs when a specialist should own the response directly. Use agents-as-tools when you need a sub-answer and want to keep going. The OpenAI Agents SDK makes this distinction explicit with handoff() versus Agent.as_tool().

# Handoff: specialist owns the reply
triage_agent = Agent(
    name="Triage",
    handoffs=[billing_agent, refund_agent],
)

# Agent-as-tool: manager stays in control
main_agent = Agent(
    name="Research assistant",
    tools=[
        summarizer.as_tool(
            tool_name="summarize_text",
            tool_description="Generate a concise summary.",
        )
    ],
)

Production Checklist

Before deploying a multi-agent system with handoffs:

  • Every orchestrator has a maximum step count with a fallback behavior
  • Agent outputs are validated against expected schemas before being passed to the next agent
  • Every handoff includes the original task/intent, not just the previous output
  • Context objects are compressed before forwarding when they exceed a defined size
  • All agent calls and handoff decisions are logged with timestamps and step counts
  • There is a fallback for every agent (what happens if the API call fails?)
  • The pipeline has been tested on a representative sample of real inputs, including edge cases
  • Each specialist has a narrow job with a clear handoff_description
  • The RECOMMENDED_PROMPT_PREFIX is included in agent instructions

Conclusion

The handoff layer is infrastructure. It doesn’t get the attention that agent capabilities do, but it’s where production systems break in practice. Getting it right — with explicit contracts, structured state, input filters, and loop guards — is the difference between a multi-agent system that works reliably and one that works only in the demos.

Start with the simplest pattern that fits your task shape. Add specialists only when the contract truly changes. Treat every handoff boundary as an API surface with a schema, validation, and error handling. And always, always include the original intent in what you pass forward.

The OpenAI Agents SDK gives you the primitives. The discipline of treating handoffs as first-class architecture is what makes them work in production.