Build with AI

How do you build an agent with the OpenAI Agents SDK?

· By Robert Scales · 3 min read

Short answer

Install the openai-agents Python package, describe each agent with a name, instructions and the Python functions it may call as tools, and run it with Runner, which handles the loop of model calls and tool calls for you. For a help desk, a triage agent hands each request to the right specialist agent, and a guardrail, a quick check that runs on the incoming message, stops off-topic requests before the main agents spend any tokens.

The Agents SDK is OpenAI's new open-source Python library for building agents. An agent, in its vocabulary, is a language model configured with instructions, tools it may call, guardrails and other agents it may hand work to. The library runs the agent loop for you: call the model, run any tools it asks for, feed the results back, and repeat until there is a final answer. If you have written that loop by hand with function calling, you will recognize each step; the SDK simply packages it.

This guide builds a small help desk for a public library: a triage agent that reads each question, two specialists that answer, and a guardrail that turns away requests that have nothing to do with the library.

Setup

You need Python and an OpenAI API key. In a terminal:

python -m venv env
source env/bin/activate
pip install openai-agents
export OPENAI_API_KEY="your-key-here"

The smallest possible agent, taken from the project's README pattern, is three lines: create Agent(name=..., instructions=...), call Runner.run_sync(agent, "your question"), and print result.final_output.

The worked example: a library help desk

Each specialist gets one tool, an ordinary Python function marked with the @function_tool decorator, which turns it into a tool the model can ask to call. The data here is made up for the example.

import asyncio
from pydantic import BaseModel
from agents import (Agent, Runner, function_tool, input_guardrail,
                    GuardrailFunctionOutput, InputGuardrailTripwireTriggered,
                    RunContextWrapper, TResponseInputItem)

# --- Tools (illustrative data) ---
@function_tool
def check_availability(title: str) -> str:
    """Return whether a book is on the shelf."""
    shelf = {"the hobbit": "2 copies available", "dune": "all copies on loan"}
    return shelf.get(title.lower(), "Not in the catalog.")

@function_tool
def account_fines(card_number: str) -> str:
    """Return the late fees owed on a library card."""
    return "$1.50 owed on 1 overdue item." if card_number == "12345" else "No fees."

# --- Specialists ---
catalog_agent = Agent(
    name="Catalog agent",
    instructions="Answer questions about books and availability. Use check_availability.",
    tools=[check_availability],
)
accounts_agent = Agent(
    name="Accounts agent",
    instructions="Answer questions about library cards, renewals and fees. "
                 "Use account_fines. Never invent amounts.",
    tools=[account_fines],
)

# --- Guardrail: is this about the library at all? ---
class TopicCheck(BaseModel):
    is_library_question: bool
    reasoning: str

topic_checker = Agent(
    name="Topic checker",
    instructions="Decide whether the message is a question for a public library help desk.",
    output_type=TopicCheck,
)

@input_guardrail
async def library_only(ctx: RunContextWrapper[None], agent: Agent,
                       input: str | list[TResponseInputItem]) -> GuardrailFunctionOutput:
    result = await Runner.run(topic_checker, input, context=ctx.context)
    return GuardrailFunctionOutput(
        output_info=result.final_output,
        tripwire_triggered=not result.final_output.is_library_question,
    )

# --- Triage ---
triage_agent = Agent(
    name="Triage agent",
    instructions="Hand off questions about books to the Catalog agent and questions "
                 "about cards, renewals or fees to the Accounts agent.",
    handoffs=[catalog_agent, accounts_agent],
    input_guardrails=[library_only],
)

async def main():
    for question in ["Is Dune available?",
                     "Do I owe anything on card 12345?",
                     "Can you do my calculus homework?"]:
        try:
            result = await Runner.run(triage_agent, question)
            print(question, "->", result.final_output)
        except InputGuardrailTripwireTriggered:
            print(question, "-> Sorry, I can only help with library questions.")

asyncio.run(main())

What happens on "Is Dune available?": the guardrail's small checker agent confirms it is a library question; the triage agent hands off to the Catalog agent; that agent calls check_availability("Dune"); the tool replies that all copies are on loan; and the Catalog agent writes the final answer. The homework question trips the guardrail, which raises an exception your code catches and answers politely.

Try it: Add a third specialist, an "Events agent" with a tool that returns this week's story times, list it in the triage agent's handoffs, and ask "When is the next story time for toddlers?" Then look at the run's trace to see which agent handled each step.

Three things worth knowing

Guardrails run alongside the agent. The SDK documentation explains the design: a quick, cheap check can stop an expensive model from working on a request it should refuse. Input guardrails only run when their agent is the first in the run, which is why library_only sits on the triage agent. Output guardrails, set with output_guardrails, check the final answer from the last agent.

Tracing is on by default. Every run is recorded as a trace, a step-by-step log of model calls, tool calls and handoffs, which is the first place to look when an agent does something unexpected.

Limits are your job. Runner.run accepts max_turns to cap how many times the loop runs. Keep tools narrow and read-only until you trust the system, and require a human to confirm any tool that changes data, spends money or sends messages.

How do hand-offs between agents work?

To the model, a handoff looks like any other tool. When you list catalog_agent in handoffs, the SDK offers the triage model a tool named after it, transfer_to_catalog_agent. If the model calls that tool, the run loop switches the active agent to the Catalog agent and continues from there. The new agent becomes responsible for the conversation and, by default, sees the entire history up to that point.

You can change that behavior with the handoff() function instead of listing an agent directly. It lets you rename the tool or rewrite its description, run an on_handoff callback the moment a transfer happens (useful for logging or for starting to fetch data early), require the model to pass structured data such as a reason for escalation through input_type, and filter the history the next agent receives with input_filter. A ready-made filter, handoff_filters.remove_all_tools, strips earlier tool calls so the specialist starts with a cleaner conversation.

The documentation also recommends telling agents about handoffs in their instructions, and provides a standard prefix, RECOMMENDED_PROMPT_PREFIX, for that purpose. In practice, the clearer each specialist's name and the triage instructions are, the more reliably the model routes each question to the right place.

Sources

  1. OpenAI Agents SDK (README) — OpenAI (GitHub), March 21, 2025
  2. Handoffs (Agents SDK documentation) — OpenAI (GitHub), March 21, 2025
  3. Guardrails (Agents SDK documentation) — OpenAI (GitHub), March 21, 2025

Get the free Guide to Artificial Intelligence

Sign up for the weekly newsletter and receive the free guide: what changed in AI this week, one lesson, one prompt to try. Unsubscribe in one click.

How we handle your address

Back to top

Display

Your choices are saved in this browser only. No data is sent.

Text size
100%
Colours
Reading
More settings
Accessibility statement