AI Agents / Developer guide

OpenAI Agents SDK Handoffs: An Offline-Tested Example

Real SDK Runner and handoffs, verified with a scripted model and explicit limits.

What this example proves

A handoff lets a specialist take over the conversation. It differs from a manager calling a specialist as a bounded tool while retaining responsibility for the reply. The official orchestration guide explains that design choice.

Our example defines a triage agent, an access specialist and a billing specialist, with no external-action tools. We tested the real installed SDK Runner and handoff implementation using a deterministic scripted model on 8 November 2026. All 10 tests passed. No OpenAI model endpoint, credential or paid API was used.

This is offline SDK execution, not just a syntax check. It is also not an end-to-end test with a real OpenAI model: the synthetic model deliberately supplies the handoff calls. See the offline-test summary for the evidence scope and scenarios.

Pinned environment and test method

Recorded isolated Python environment
ComponentVersion
Python3.12.14
openai-agents0.24.0
openai3.46.0
pydantic2.14.3
mcp2.3.0
httpx22.16.0

The original sample was imported without editing it. A scripted implementation of the SDK Model interface returned tool-call and final-message objects. The real Runner consumed those objects, performed the handoff, continued with the specialist’s instructions and returned a real RunResult.

For the original main() function, the harness wrapped its Runner.run call only to inject RunConfig(model=scripted_model, tracing_disabled=True) and observation hooks. The wrapped call still executed the real Runner; the sample’s max_turns=6 was preserved.

Provider credentials were removed from the test process, tracing was disabled, and outbound HTTP was blocked. A guard self-test confirmed that an attempted send was stopped before connection. No network attempts were recorded during the SDK test cases. The controls were process-level; there was no independent packet capture.

The original sample: real-provider setup is a separate path

The downloadable sample below retains its original provider-oriented configuration. The official quickstart documents SDK installation and API-key setup. To match the top-level SDK version exercised in our offline test:

python -m venv .venv
# Activate .venv for your operating system, then:
python -m pip install openai-agents==0.24.0
python -m pip show openai-agents

Important distinction: running this sample unchanged requires OPENAI_MODEL and a valid provider configuration, normally OPENAI_API_KEY, and can incur API charges. It does not reproduce our offline harness by itself. A fake model-name string alone does not create an offline model.

The harness supplied an actual scripted Model object through RunConfig instead. Its outputs are synthetic and cannot establish the routing quality of a real language model.

import asyncio
import os

from agents import Agent, Runner

model = os.environ["OPENAI_MODEL"]

access = Agent(
    name="Access specialist",
    handoff_description="Handles sign-in and account access questions.",
    instructions="Explain account recovery. Never request a password.",
    model=model,
)
billing = Agent(
    name="Billing specialist",
    handoff_description="Handles billing questions.",
    instructions="Explain billing concepts. Do not promise a refund.",
    model=model,
)
triage = Agent(
    name="Triage",
    instructions="For sign-in or access, hand off to Access specialist. "
                 "For billing, hand off to Billing specialist.",
    handoffs=[access, billing],
    model=model,
)

async def main():
    result = await Runner.run(
        triage, "I cannot sign in. How can I recover access?", max_turns=6
    )
    print("Final agent:", result.last_agent.name)
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())

Download the unchanged sample. The turn cap is a demonstration bound, not a production default. No real-provider execution is claimed here.

Observed Runner and handoff results

Observed offline behavior using the real SDK
ScenarioChecked result
Access handoffTriage → Access specialist; the specialist produced the scripted final output
Billing handoffTriage → Billing specialist; the other specialist’s instructions were used
Original main() with injected modelPrinted the final-agent name and synthetic output; retained max_turns=6
Conversation historyHandoff call/output items and matching call_id reached the specialist
Direct final answerRunner stopped at Triage without a handoff
max_turns=1MaxTurnsExceeded prevented a second model call
Unknown handoff targetModelBehaviorError was raised

The captured output from the original main() under the offline harness was:

Final agent: Access specialist
SYNTHETIC MAIN ANSWER

SYNTHETIC MAIN ANSWER was prescribed by the test model. It is an actual captured test output, not a response from an OpenAI model. The tested Agent arguments, handoff_description, handoffs list, Runner.run, max_turns, last_agent and final_output worked with SDK 0.24.0. No incompatibility was observed in those exercised APIs.

Argument validation: typed and untyped handoffs differ

The sample passes Agent objects directly in handoffs and does not define input_type. In the installed SDK, that simple handoff did not parse its unused argument payload: a deliberately malformed JSON payload still transferred control. It should not be described as a general argument-validation boundary.

An additional, separate test graph used handoff(..., input_type=Payload, on_handoff=callback). There, malformed JSON was rejected with ModelBehaviorError and the callback was not called. That typed graph is not part of the downloadable sample.

The initial nine-test run had one incorrect test expectation about the untyped case. We inspected the installed SDK source and split the check into typed and untyped cases; the final ten-test suite passed. Neither the sample nor the installed SDK was changed to obtain a pass.

What remains untested

Real-model routing decisions, mixed-topic handling, answer quality, authentication, model availability, rate limits and service latency were not tested. Neither were streaming, persisted sessions, concurrency or publishing integrations. The result does not establish compatibility with every SDK version.

A handoff changes agent ownership; it does not grant permission to change an account or publish content. Constrain tool permissions outside the model, validate the arguments your application consumes and add human approval where needed.

Before adding specialists, use the agents-versus-workflows guide to decide whether a fixed path already solves the task. A real-provider evaluation is a separate step from the offline SDK checks reported here.

Sources & verification

Documentation reviewed and scoped tests completed on 8 November 2026. Results and limitations above apply only to the recorded environments. No paid API requests were made.

Read the machine-readable test summary · Editorial policy.