OpenAI Agents SDK: Handoffs, Tools & Guardrails in 10 Minutes

The OpenAI Agents SDK (released March 2025) flips the mental model from 'chat loop' to 'agent object'. In this post we build a two-agent pipeline with tools, a handoff, and a guardrail — in about 60 lines of Python.

5 min read

If you've been building with the raw OpenAI Chat Completions API, you've probably written the same boilerplate loop over and over: send messages, check for tool_calls, invoke the tool, append the result, call the API again. The OpenAI Agents SDK (released March 2025) was designed to eliminate exactly that ceremony. It introduces a higher-level Agent object that manages the loop, tools, and multi-agent routing for you.

This post builds a working two-agent pipeline — a Researcher that looks up information and hands off to a Writer that summarises it — in under 60 lines of Python.

The Three Core Primitives

Everything in the Agents SDK is built on three objects:

PrimitiveWhat it does
AgentDefines an LLM + instructions + a list of tools it can use
RunnerExecutes an agent (or a chain of agents) and manages the loop
ToolA Python function decorated with @function_tool that the agent can call

That's deliberately minimal. If you know what those three things do, you can build almost anything.

Install and Set Up

Terminal
pip install openai-agents
export OPENAI_API_KEY="sk-..."

The SDK uses gpt-4.1 by default. You can override per-agent with model="gpt-4o-mini" if you want to cut costs during development.

Step 1: Define a Tool

A tool is just a Python function with a docstring. The SDK uses the docstring as the tool description and infers the JSON schema from the type annotations.

from agents import function_tool
 
@function_tool
def search_web(query: str) -> str:
    """Search the web and return a short summary of the top result."""
    # In production, call a real search API (Tavily, Brave, etc.)
    # For this demo we return a stub.
    return f"Top result for '{query}': AI agents are autonomous systems that use LLMs to reason and act."

That's it — no JSON schema to write, no manual registration.

Step 2: Create Two Agents

from agents import Agent
 
researcher = Agent(
    name="Researcher",
    instructions="You are a research assistant. Use the search_web tool to find information, then hand off to the Writer agent with a concise summary of what you found.",
    tools=[search_web],
    model="gpt-4.1",
)
 
writer = Agent(
    name="Writer",
    instructions="You are a technical writer. Take the research summary you receive and write a clear, 3-sentence explanation suitable for a developer blog.",
    model="gpt-4.1",
)

Step 3: Add a Handoff

A handoff tells the Researcher that it can transfer control to the Writer once it has finished gathering information. It appears in the agent's tool list just like a regular tool.

from agents import handoff
 
researcher.tools.append(handoff(writer))

When the Researcher calls transfer_to_Writer (the handoff tool the SDK auto-generates), the Runner passes control and accumulated context to the Writer agent.

Step 4: Run the Pipeline

import asyncio
from agents import Runner
 
async def main():
    result = await Runner.run(
        researcher,
        input="What are AI agents and why do developers care about them?",
    )
    print(result.final_output)
 
asyncio.run(main())

The Runner handles the full agentic loop:

  1. Sends the input to the Researcher
  2. The Researcher calls search_web, gets a result
  3. The Researcher calls the transfer_to_Writer handoff
  4. The Writer generates the final output
  5. result.final_output contains the Writer's response

Adding a Guardrail

Guardrails let you validate input before it reaches the agent. A common use case is blocking off-topic requests:

from agents import input_guardrail, GuardrailFunctionOutput, RunContextWrapper
from pydantic import BaseModel
 
class TopicCheck(BaseModel):
    is_on_topic: bool
    reason: str
 
@input_guardrail
async def topic_guardrail(
    ctx: RunContextWrapper, agent: Agent, input: str
) -> GuardrailFunctionOutput:
    """Only allow questions about AI and software development."""
    result = await Runner.run(
        Agent(
            name="TopicChecker",
            instructions="Decide if the user's question is about AI or software development. Respond with JSON.",
            output_type=TopicCheck,
        ),
        input=input,
        context=ctx.context,
    )
    check: TopicCheck = result.final_output
    return GuardrailFunctionOutput(
        output_info=check,
        tripwire_triggered=not check.is_on_topic,
    )
 
# Add the guardrail to the researcher
researcher = Agent(
    name="Researcher",
    instructions="...",
    tools=[search_web, handoff(writer)],
    input_guardrails=[topic_guardrail],
)

If tripwire_triggered is True, the SDK raises an InputGuardrailTripwireTriggered exception before the agent even runs.

Agents SDK vs. Raw Chat Completions

Raw Chat CompletionsAgents SDK
Tool loopManual (you write it)Automatic
Multi-agent routingManualHandoffs
TracingNone built-inBuilt-in trace per run
Input validationManualGuardrails
Best forFull control, simple one-shot callsAgentic workflows, multi-step tasks

Use raw Chat Completions when you need fine-grained control over every API call (e.g., streaming partial tokens into a UI). Reach for the Agents SDK the moment your workflow involves tool calls, retries, or multiple agents handing off to each other.

What's Next

Tomorrow we'll put the two SDKs side by side: the same weather-lookup agent built with the Anthropic Claude SDK and with the OpenAI Agents SDK, with a line-by-line comparison of how tool schemas, invocation flows, and error handling differ.