Track
OpenAI Agents SDK Tutorial: Building AI Systems That Take Action
The field of artificial intelligence is shifting from systems that simply respond to queries to those that can act independently. OpenAI’s new Agents SDK is at the forefront of this trend, providing developers with a practical framework for building AI applications that make decisions and perform actions on their own. This toolkit represents the next evolution in AI development, where systems don’t just answer questions but actively solve problems.
The Agents SDK combines large language models with the ability to use tools and coordinate between specialized agents. Built with Python, it offers a balance between simplicity and power — using just a few core concepts like agents, tools, handoffs, and guardrails. This approach makes it accessible to developers who want to create sophisticated AI systems without managing the complex underlying mechanics of agent behavior.
In this tutorial, we’ll explore how to build practical applications with the OpenAI Agents SDK. Starting with basic agent creation, we’ll progress to implementing tools, coordinating multiple agents, and ensuring safety through guardrails. The knowledge gained will enable you to develop AI systems that can handle various tasks, providing you with skills relevant to current AI development approaches.
If you're new to working with OpenAI, check out our OpenAI Fundamentals skill track to get up to speed. You can also check out our video tutorial for OpenAI Agents SDK below.
Prerequisites for Working with OpenAI Agents SDK
Before diving into the OpenAI Agents SDK, it’s important to understand several foundational concepts and set up your environment correctly. This section covers everything you need to know before writing your first agent.
Required knowledge
To get the most out of this tutorial, you should be comfortable with:
- Intermediate Python programming: You should understand functions, classes, async/await patterns, and type hints
- Basic OpenAI API usage: Familiarity with sending requests to OpenAI models and handling responses
- Large language models (LLMs): Understanding of how LLMs work conceptually and their capabilities/limitations
- Pydantic: Basic knowledge of using Pydantic for data validation will be helpful, as the Agents SDK uses it extensively
- Asynchronous programming: Familiarity with async/await patterns in Python, as the Agents SDK is built around asynchronous execution
If you need to strengthen your knowledge in some of these areas, DataCamp offers several excellent resources:
- Working with the OpenAI API — Learn the fundamentals of interacting with OpenAI’s models.
- OpenAI Fundamentals Track — A comprehensive learning path for OpenAI technologies.
- GPT-4.5 overview — Learn about the latest capabilities of GPT models.
- Function calling with GPT-4.5 — Essential background for understanding tool usage in agents.
- Using the GPT-4.5 API — Practical guide to working with advanced models.
Key concepts for agents
Before we start coding, let’s clarify a few key concepts that are central to the Agents SDK:
- Agents: AI systems that can use tools and make decisions to accomplish tasks
- Tools: Functions that agents can call to perform actions like searching the web or accessing databases
- Handoffs: Mechanisms for transferring control between specialized agents
- Guardrails: Safety measures that validate inputs and outputs to ensure appropriate behavior
Understanding these concepts and their relationships will help you build more effective agent-based applications. You can learn more in our guide to understanding AI agents.
OpenAI Agents Environment Setup
Let’s set up our development environment with everything needed to work with the OpenAI Agents SDK:
Installing required packages
First, create and activate a virtual environment:
# Create and activate a virtual environment (run in your terminal)
python -m venv agents-env
source agents-env/bin/activate # On Windows: agents-env\Scripts\activate
Next, install the OpenAI Agents SDK and python-dotenv:
pip install openai-agents python-dotenv
Using python-dotenv for API key management
When working with API keys, it’s a best practice to keep them out of your code. The python-dotenv
package provides a secure way to manage environment variables:
- Create a file named
.env
in your project directory - Add your OpenAI API key to this file:
OPENAI_API_KEY=your-api-key-here
3. Load and use the environment variables in your code:
import os
from dotenv import load_dotenv
from agents import Agent, Runner
# Load environment variables from .env file
load_dotenv()
# Access your API key
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY not found in .env file")
This approach keeps sensitive information out of your code, which is especially important if you’re sharing or publishing your work.
Working with async functions
The OpenAI Agents SDK uses asynchronous programming extensively. Here’s how to work with async functions in different environments:
In Python scripts (.py files)
In regular Python scripts, you’ll need to use asyncio.run()
to execute async functions:
import asyncio
from agents import Agent, Runner
async def main():
agent = Agent(
name="Test Agent",
instructions="You are a helpful assistant that provides concise responses."
)
result = await Runner.run(agent, "Hello! Are you working correctly?")
print(result.final_output)
if __name__ == "__main__":
asyncio.run(main()) # Run the async function
In Jupyter notebooks
Jupyter notebooks already have an event loop running, so you should NOT use asyncio.run()
. Instead, you can await async functions directly:
from agents import Agent, Runner
# No need for asyncio.run() in notebooks
agent = Agent(
name="Test Agent",
instructions="You are a helpful assistant that provides concise responses."
)
result = await Runner.run(agent, "Hello! Are you working correctly?")
print(result.final_output)
If you try to use asyncio.run()
in a notebook, you'll encounter errors like:
RuntimeError: asyncio.run() cannot be called from a running event loop
This is one of the most common adjustments you’ll need to make when copying code between scripts and notebooks. Throughout this tutorial, I’ll primarily use the notebook-friendly syntax since most readers will be using Jupyter.
Testing your installation
Let’s make sure everything is set up correctly:
from agents import Agent, Runner
from dotenv import load_dotenv
load_dotenv()
# For Jupyter notebooks:
agent = Agent(
name="Test Agent",
instructions="You are a helpful assistant that provides concise responses."
)
result = await Runner.run(agent, "Hello! Are you working correctly?")
print(result.final_output)
# For Python scripts, you'd use:
# async def test_installation():
# agent = Agent(
# name="Test Agent",
# instructions="You are a helpful assistant that provides concise responses."
# )
# result = await Runner.run(agent, "Hello! Are you working correctly?")
# print(result.final_output)
#
# if __name__ == "__main__":
# asyncio.run(test_installation())
# Hello! Yes, I'm here and ready to help. How can I assist you today?
With this setup complete, you’re ready to begin building applications with the OpenAI Agents SDK.
Getting Started with OpenAI Agents SDK
At the heart of the OpenAI Agents SDK is the Agent
class, which serves as your primary interface for creating AI systems that can understand and act upon instructions. Let's explore how to create and configure agents for different scenarios.
An agent in this SDK represents an AI system capable of following instructions and, optionally, using tools to accomplish tasks. Creating a basic agent requires just a few essential parameters:
from agents import Agent
basic_agent = Agent(
name="My First Agent",
instructions="You are a helpful assistant that provides factual information.",
model="gpt-4o" # Optional: defaults to "gpt-4o" if not specified
)
The three primary components of an agent are:
- Name: An identifier for your agent that helps with logging and debugging
- Instructions: The core “system prompt” that defines the agent’s behavior and purpose
- Model: The underlying language model powering the agent (defaults to GPT-4o)
While this basic setup is enough to get started, the Agent
class offers several additional model configuration options that give you more control over LLM's behavior:
from agents import ModelSettings
advanced_agent = Agent(
name="Advanced Assistant",
instructions="""You are a professional, concise assistant who always provides
accurate information. When you don't know something, clearly state that.
Focus on giving actionable insights when answering questions.""",
model="gpt-4o",
model_settings=ModelSettings(
temperature=0.3, # Lower for more deterministic outputs (0.0-2.0)
max_tokens=1024, # Maximum length of response
),
tools=[] # We'll cover tools in a later section
)
Instructions and configuration
The instructions parameter is perhaps the most important aspect of agent design. It functions as a “system prompt” that guides the agent’s behavior, tone, and capabilities. Writing effective instructions is both an art and a science:
Best practices for writing instructions
- Be specific: Clearly define the agent’s role, personality, and limitations
- Set boundaries: Explicitly state what topics or actions the agent should avoid
- Define interaction patterns: Explain how the agent should handle various types of inputs
- Establish knowledge boundaries: Clarify what the agent should know and when it should acknowledge uncertainty
The Agent
also includes a description
parameter, which is a human-readable description of the agent, used when the agent is used inside tools/handoffs.
Configuration options
Beyond instructions, you can fine-tune agent behavior using several configuration parameters:
- temperature: Controls randomness in responses (0.0–2.0)
- Lower values (0.1–0.4) produce more deterministic, focused responses
- Higher values (0.7–1.0) create more creative, varied outputs
- max_tokens: Limits the length of agent responses
- Useful for ensuring concise outputs or controlling costs
- The default varies by model but is typically high enough for most use cases
- model: Selects the underlying LLM
- “gpt-4o” offers the best performance for most use cases
- “gpt-4o-mini” provides a good balance of performance and cost
- “gpt-3.5-turbo” is available for less complex tasks where speed and cost are priorities
These configuration options provide a powerful framework for customizing agent behavior to suit various applications. We encourage you to experiment with different settings to discover how they affect your agent’s responses and find the optimal configuration for your specific use case.
Open AI Agents SKD Example: Building a specialized weather assistant
Now, let’s bring these concepts together by building a practical example: a specialized weather information assistant. This example demonstrates how to create an agent with well-defined expertise, capabilities, and limitations:
from agents import Agent, Runner
from dotenv import load_dotenv
# Load environment variables (API key)
load_dotenv()
# Define detailed instructions for our weather assistant
weather_instructions = """
You are a weather information assistant who helps users understand weather patterns and phenomena.
YOUR EXPERTISE:
- Explaining weather concepts and terminology
- Describing how different weather systems work
- Answering questions about climate and seasonal patterns
- Explaining the science behind weather events
LIMITATIONS:
- You cannot provide real-time weather forecasts for specific locations
- You don't have access to current weather data
- You should not make predictions about future weather events
STYLE:
- Use clear, accessible language that non-meteorologists can understand
- Include interesting weather facts when relevant
- Be enthusiastic about meteorology and climate science
"""
# Create our specialized weather assistant
weather_assistant = Agent(
name="WeatherWise",
instructions=weather_instructions,
model="gpt-3.5-turbo",
model_settings=ModelSettings(
temperature=0.5, # Balanced temperature for natural but focused responses
max_tokens=256, # Maximum length of response
)
)
Running your first agent
Once you’ve created an agent, you can run it using the Runner
class. This class handles the execution of agent tasks and manages the conversation flow:
# For Jupyter notebooks:
result = await Runner.run(
weather_assistant, "Can you tell me about the relationship between climate change and extreme weather events?"
)
print(result.final_output)
—--------
Absolutely! Climate change is closely linked to the increase in frequency and intensity of extreme weather events. As the Earth's climate warms due to the buildup of greenhouse gases in the atmosphere, it disrupts the balance of our planet's climate systems.
Here's how climate change influences extreme weather events:
1. **Heatwaves**: Rising global temperatures lead to more frequent and severe heatwaves. These events can have serious impacts on human health, agriculture, and ecosystems.
2. **Intense Storms**: Warmer oceans provide more energy to fuel hurricanes, typhoons, and other tropical storms, leading to stronger and more destructive events.
3. **Heavy Rainfall**: A warmer atmosphere can hold more moisture, resulting in heavier rainfall during storms. This can lead to flooding and landslides.
4. **Droughts**: Climate change can exacerbate drought conditions in certain regions, impacting water resources, agriculture, and ecosystems.
5. **Wildfires**: Higher temperatures and drier conditions increase the likelihood of wildfires, which can be more frequent and intense.
It's important to note that while climate change doesn't directly cause specific weather events, it can increase the likelihood and severity of extreme weather occurrences. Scientists continue to study these connections to better understand and prepare for the impacts of
For Python scripts, you would need to use the asyncio
approach:
# For Python scripts:
import asyncio
async def run_agent_example():
result = await Runner.run(weather_assistant, "Can you tell me about the relationship between climate change and extreme weather events?")
print(result.final_output)
if __name__ == "__main__":
asyncio.run(run_agent_example())
In this section, we’ve covered the fundamentals of creating and running agents with the OpenAI Agents SDK. In the next section, we’ll expand on these concepts by adding tools to our agents, which will significantly enhance their capabilities.
Working with Tools
The real power of the OpenAI Agents SDK emerges when you equip your agents with tools. Tools enable agents to interact with external systems, access data, and perform actions that extend beyond simple text generation. The Agents SDK supports three main types of tools: hosted tools, function tools, and agents as tools.
Hosted tools
Hosted tools run on OpenAI’s servers alongside the language models. These tools provide built-in capabilities without requiring you to implement complex functionality.
WebSearchTool: Building a research assistant
The WebSearchTool
gives your agent the ability to search the web for up-to-date information. This is particularly valuable for tasks requiring current knowledge beyond the model's training data.
from agents import Agent, Runner, WebSearchTool
from dotenv import load_dotenv
load_dotenv()
# Create a research assistant with web search capability
research_assistant = Agent(
name="Research Assistant",
instructions="""You are a research assistant that helps users find and summarize information.
When asked about a topic:
1. Search the web for relevant, up-to-date information
2. Synthesize the information into a clear, concise summary
3. Structure your response with headings and bullet points when appropriate
4. Always cite your sources at the end of your response
If the information might be time-sensitive or rapidly changing, mention when the search was performed.
""",
tools=[WebSearchTool()]
)
async def research_topic(topic):
result = await Runner.run(research_assistant, f"Please research and summarize: {topic}. Only return the found links with very minimal text.")
return result.final_output
# Usage example (in Jupyter notebook)
summary = await research_topic("Latest developments in personal productivity apps.")
print(summary[:512])
—----------------
Here are some recent developments in personal productivity apps:
- **AI Integration in Productivity Tools**: Startups like Anthropic are developing AI agents capable of performing routine tasks across different applications, aiming to streamline workflows and reduce the need for multiple apps. ([ft.com](https://www.ft.com/content/a0e54dd5-b270-42cc-8c4c-18a0b8b3e6cc?utm_source=openai))
- **Read AI's Expansion**: Read AI, a productivity startup, secured $50 million in funding, valuing the company at $450 m
In this example, we’ve created a research assistant that can search the web and synthesize information into a coherent summary. The WebSearchTool
doesn't require any parameters to function, but you can customize its behavior as needed:
# Customized search tool with location context
location_aware_search = WebSearchTool(
user_location="San Francisco, CA", # Provides geographic context for local search queries
search_context_size=3 # Number of search results to consider in the response
)
The user_location
parameter is particularly useful for queries that benefit from geographic context, such as local services or regional information. The search_context_size
parameter helps control how many search results the model considers when formulating its response.
Function tools
Function tools allow you to extend your agent with any Python function. This is where the real flexibility of the Agents SDK shines, enabling integration with any API, database, or local service.
Weather forecast tool
Let’s create a practical example that integrates with a third-party weather API:
import os
import requests
from datetime import datetime
from typing import Optional, List
from dataclasses import dataclass
from agents import Agent, Runner, function_tool
from dotenv import load_dotenv
load_dotenv()
@dataclass
class WeatherInfo:
temperature: float
feels_like: float
humidity: int
description: str
wind_speed: float
pressure: int
location_name: str
rain_1h: Optional[float] = None
visibility: Optional[int] = None
@function_tool
def get_weather(lat: float, lon: float) -> str:
"""Get the current weather for a specified location using OpenWeatherMap API.
Args:
lat: Latitude of the location (-90 to 90)
lon: Longitude of the location (-180 to 180)
"""
# Get API key from environment variables
WEATHER_API_KEY = os.getenv("OPENWEATHERMAP_API_KEY")
# Build URL with parameters
url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={WEATHER_API_KEY}&units=metric"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
# Extract weather data from the response
weather_info = WeatherInfo(
temperature=data["main"]["temp"],
feels_like=data["main"]["feels_like"],
humidity=data["main"]["humidity"],
description=data["weather"][0]["description"],
wind_speed=data["wind"]["speed"],
pressure=data["main"]["pressure"],
location_name=data["name"],
visibility=data.get("visibility"),
rain_1h=data.get("rain", {}).get("1h"),
)
# Build the response string
weather_report = f"""
Weather in {weather_info.location_name}:
- Temperature: {weather_info.temperature}°C (feels like {weather_info.feels_like}°C)
- Conditions: {weather_info.description}
- Humidity: {weather_info.humidity}%
- Wind speed: {weather_info.wind_speed} m/s
- Pressure: {weather_info.pressure} hPa
"""
return weather_report
except requests.exceptions.RequestException as e:
return f"Error fetching weather data: {str(e)}"
This weather API function tool serves as a bridge between the OpenWeatherMap API and our agent. It fetches real-time weather data by geographic coordinates and formats it into a readable report.
The code defines a WeatherInfo
data class to structure the data with typed fields (temperature, humidity, etc.), making it organized and maintainable. The @function_tool
decorator transforms our Python function into a tool the agent can use, automatically creating a schema from the function's signature and docstring.
When called, the function securely retrieves an API key from environment variables, makes an HTTP request to OpenWeatherMap, and processes the JSON response.
It handles optional data fields like rainfall safely and formats everything into a clear weather report, including error handling if the API request fails. This allows our agent to provide current weather information in a consistent, human-readable format without needing to understand API details.
# Create a weather assistant
weather_assistant = Agent(
name="Weather Assistant",
instructions="""You are a weather assistant that can provide current weather information.
When asked about weather, use the get_weather tool to fetch accurate data.
If the user doesn't specify a country code and there might be ambiguity,
ask for clarification (e.g., Paris, France vs. Paris, Texas).
Provide friendly commentary along with the weather data, such as clothing suggestions
or activity recommendations based on the conditions.
""",
tools=[get_weather]
)
To run the weather assistant:
async def main():
runner = Runner()
simple_request = await runner.run(weather_assistant, "What are your capabilities?")
request_with_location = await runner.run(weather_assistant, "What's the weather like in Tashkent right now?")
print(simple_request.final_output)
print("-"*70)
print(request_with_location.final_output)
await main()
Output:
I can provide you with the current weather conditions for any location around the world. Just give me the location details, and I'll fetch the weather data for you. I can also offer tips on activities or clothing based on the weather. If you have a specific city in mind, let me know, and I'll get to work!
----------------------------------------------------------------------
The weather in Tashkent is currently lovely with a clear sky. It's around 19.8°C, but it might feel slightly cooler at 18.6°C. The humidity is quite low at 26%, making it a pleasant day to be outdoors. A gentle breeze is blowing at 3.09 m/s, and the air pressure is steady at 1023 hPa.
It's a great time for a picnic in the park or a leisurely walk. Light clothing should be perfect for this weather. Enjoy your day in Tashkent!
The response proves that the agent used the weather tool successfully. It isolated the city name from the prompt, used its own knowledge base to get its coordinates, and passed them as lat
and lon
to the tool.
Agents as tools
The Agents SDK allows you to use agents themselves as tools, enabling a hierarchical structure where specialist agents work under a coordinator. This is a powerful pattern for complex workflows.
from agents import Agent, Runner
from dotenv import load_dotenv
load_dotenv()
# Specialist agents
note_taking_agent = Agent(
name="Note Manager",
instructions="You help users take and organize notes efficiently.",
# In a real application, this agent would have note-taking tools
)
task_management_agent = Agent(
name="Task Manager",
instructions="You help users manage tasks, deadlines, and priorities.",
# In a real application, this agent would have task management tools
)
# Coordinator agent that uses specialists as tools
productivity_assistant = Agent(
name="Productivity Assistant",
instructions="""You are a productivity assistant that helps users organize their work and personal life.
For note-taking questions or requests, use the note_taking tool.
For task and deadline management, use the task_management tool.
Help the user decide which tool is appropriate based on their request,
and coordinate between different aspects of productivity.
""",
tools=[
note_taking_agent.as_tool(
tool_name="note_taking",
tool_description="For taking, organizing, and retrieving notes and information"
),
task_management_agent.as_tool(
tool_name="task_management",
tool_description="For managing tasks, setting deadlines, and tracking priorities"
)
]
)
In this example:
- We create two specialist agents, each with a focused domain of expertise.
- We then create a coordinator agent that can delegate to these specialists.
- We convert each specialist agent into a tool using the
.as_tool()
method, specifying:
- A
tool_name
that the coordinator will use to reference the tool. - A
tool_description
that helps the coordinator understand when to use this tool.
This pattern allows you to create complex agent systems while maintaining the separation of concerns. Each specialist agent can have its own set of tools and expertise, while the coordinator manages the user interaction and delegates it to the appropriate specialist.
To use the productivity assistant:
async def main():
runner = Runner()
result = await runner.run(productivity_assistant, "I need to keep track of my project deadlines")
print(result.final_output)
await main()
Tools transform agents from simple conversational assistants into powerful systems capable of taking meaningful actions in the world. While we’ve covered the basics of creating and using tools, this is just the beginning of what’s possible. For more advanced usage, including error handling in function tools, refer to the official documentation.
Now that we understand how to equip our agents with tools, the next challenge is handling and structuring their outputs. In the next section, we’ll explore techniques for processing agent responses, from basic text outputs to complex structured data, ensuring we get the exact information format our applications need.
Understanding OpenAI Agent Outputs
When working with agents, getting structured information rather than free-form text can make your applications more reliable. The OpenAI Agents SDK provides a clean, built-in way to receive structured outputs directly from agents.
Structured outputs with Pydantic models
The SDK allows you to define exactly what data structure you want your agent to return by specifying an output_type
parameter when creating an agent:
from pydantic import BaseModel
from typing import List, Optional
from agents import Agent, Runner
from dotenv import load_dotenv
load_dotenv()
True
First, we define our data models using Pydantic:
# Define person data model
class Person(BaseModel):
name: str
role: Optional[str]
contact: Optional[str]
# Define meeting data model
class Meeting(BaseModel):
date: str
time: str
location: Optional[str]
duration: Optional[str]
# Define task data model
class Task(BaseModel):
description: str
assignee: Optional[str]
deadline: Optional[str]
priority: Optional[str]
# Define the complete email data model
class EmailData(BaseModel):
subject: str
sender: Person
recipients: List[Person]
main_points: List[str]
meetings: List[Meeting]
tasks: List[Task]
next_steps: Optional[str]
These models define the structure we want our extracted data to follow. Each class represents a specific type of information we want to extract from an email.
Now, we create an agent that will output data in our structured format by setting the output_type
parameter:
# Create an email extraction agent with structured output
email_extractor = Agent(
name="Email Extractor",
instructions="""You are an assistant that extracts structured information from emails.
When given an email, carefully identify:
- Subject and main points
- People mentioned (names, roles, contact info)
- Meetings (dates, times, locations)
- Tasks or action items (with assignees and deadlines)
- Next steps or follow-ups
Extract this information as structured data. If something is unclear or not mentioned,
leave those fields empty rather than making assumptions.
""",
output_type=EmailData, # This tells the agent to return data in EmailData format
)
When you specify the output_type
, the agent will automatically produce structured data instead of plain text responses. This eliminates the need for manual JSON parsing or regex extraction.
Let’s use this extractor with a sample email:
sample_email = """
From: Alex Johnson <alex.j@techcorp.com>
To: Team Development <team-dev@techcorp.com>
CC: Sarah Wong <sarah.w@techcorp.com>, Miguel Fernandez <miguel.f@techcorp.com>
Subject: Project Phoenix Update and Next Steps
Hi team,
I wanted to follow up on yesterday's discussion about Project Phoenix and outline our next steps.
Key points from our discussion:
- The beta testing phase has shown promising results with 85% positive feedback
- We're still facing some performance issues on mobile devices
- The client has requested additional features for the dashboard
Let's schedule a follow-up meeting this Friday, June 15th at 2:00 PM in Conference Room B. The meeting should last about 1.5 hours, and we'll need to prepare the updated project timeline.
Action items:
1. Sarah to address the mobile performance issues by June 20th (High priority)
2. Miguel to create mock-ups for the new dashboard features by next Monday
3. Everyone to review the beta testing feedback document and add comments by EOD tomorrow
If you have any questions before Friday's meeting, feel free to reach out.
Best regards,
Alex Johnson
Senior Project Manager
(555) 123-4567
"""
Now processing the email becomes much simpler because the SDK handles the conversion:
async def process_email(email_text):
runner = Runner()
result = await runner.run(
email_extractor,
f"Please extract information from this email:\n\n{email_text}"
)
# The result is already a structured EmailData object
return result
# Process the sample email
result = await process_email(sample_email)
# Display the extracted information
result = result.final_output
print(f"Subject: {result.subject}")
print(f"From: {result.sender.name} ({result.sender.role})")
print("\nMain points:")
for point in result.main_points:
print(f"- {point}")
print("\nMeetings:")
for meeting in result.meetings:
print(f"- {meeting.date} at {meeting.time}, Location: {meeting.location}")
print("\nTasks:")
for task in result.tasks:
print(f"- {task.description}")
print(
f" Assignee: {task.assignee}, Deadline: {task.deadline}, Priority: {task.priority}"
)
—----------
Subject: Project Phoenix Update and Next Steps
From: Alex Johnson (Senior Project Manager)
Main points:
- 85% positive feedback from beta testing
- Performance issues on mobile devices
- Client requested additional dashboard features
Meetings:
- June 15th at 2:00 PM, Location: Conference Room B
Tasks:
- Address mobile performance issues
Assignee: Sarah, Deadline: June 20th, Priority: High
- Create mock-ups for new dashboard features
Assignee: Miguel, Deadline: Next Monday, Priority: None
- Review beta testing feedback document and add comments
Assignee: Everyone, Deadline: EOD tomorrow, Priority: None
This code is much cleaner than our earlier approach because:
- We don’t need to manually extract and parse JSON from the response.
- The SDK handles the conversion from the agent’s output to our Pydantic model.
- We can directly access the structured data properties (like
result.final_output.subject
). - Type validation happens automatically, so we know the data matches our model.
Working with different output types
The output_type
parameter works with any type that can be wrapped in a Pydantic TypeAdapter
:
# For simple lists
agent_with_list_output = Agent(
name="List Generator",
instructions="Generate lists of items based on the user's request.",
output_type=list[str], # Returns a list of strings
)
# For dictionaries
agent_with_dict_output = Agent(
name="Dictionary Generator",
instructions="Create key-value pairs based on the input.",
output_type=dict[
str, int
], # Returns a dictionary with string keys and integer values
)
# For simple primitive types
agent_with_bool_output = Agent(
name="Decision Maker",
instructions="Answer yes/no questions with True or False.",
output_type=bool, # Returns a boolean
)
Benefits of structured outputs
Using the output_type
parameter offers several advantages:
- Direct integration: Your agent outputs are automatically available as Python objects.
- Type safety: The SDK ensures that outputs match your defined structure.
- Simpler code: No need for manual JSON parsing or error handling.
- Better performance: The SDK handles the conversion efficiently.
- IDE support: Your IDE can provide autocompletion for the structured output properties.
By defining clear data models and using the output_type
parameter, your agents can produce exactly the data structures your application needs, making integration seamless and reducing the complexity of your code.
In the next section, we’ll explore handoffs between agents, allowing you to create specialized agents that can work together on complex tasks. These handoffs can use structured outputs to pass information between agents in a consistent, type-safe manner.
Handoffs: Delegating Between Agents
In complex applications, different tasks often require different areas of expertise. The OpenAI Agents SDK supports “handoffs,” which allow one agent to delegate control to another specialized agent. This feature is particularly valuable when building systems that handle diverse user requests, such as customer support applications where different agents might handle billing inquiries, technical support, or account management.
Creating basic handoffs
At its simplest, handoffs allow you to connect multiple agents so they can transfer control when appropriate. Let’s create a simple customer service system with a triage agent that can hand off to specialists:
from agents import Agent, handoff, Runner
from dotenv import load_dotenv
load_dotenv()
# Create specialist agents
billing_agent = Agent(
name="Billing Agent",
instructions="""You are a billing specialist who helps customers with payment issues.
Focus on resolving billing inquiries, subscription changes, and refund requests.
If asked about technical problems or account settings, explain that you specialize
in billing and payment matters only.""",
)
technical_agent = Agent(
name="Technical Agent",
instructions="""You are a technical support specialist who helps with product issues.
Assist users with troubleshooting, error messages, and how-to questions.
Focus on resolving technical problems only.""",
)
# Create a triage agent that can hand off to specialists
triage_agent = Agent(
name="Customer Service",
instructions="""You are the initial customer service contact who helps direct
customers to the right specialist.
If the customer has billing or payment questions, hand off to the Billing Agent.
If the customer has technical problems or how-to questions, hand off to the Technical Agent.
For general inquiries or questions about products, you can answer directly.
Always be polite and helpful, and ensure a smooth transition when handing off to specialists.""",
handoffs=[billing_agent, technical_agent], # Direct handoff to specialist agents
)
In this example, we’ve created a system with three agents:
- Two specialist agents with focused expertise
- One triage agent that can delegate to the specialists
Notice how we simply include the specialist agents in the handoffs
parameter of the triage agent. The Agents SDK automatically creates appropriate handoff tools that the triage agent can use when needed.
Let’s see the system in action:
async def handle_customer_request(request):
runner = Runner()
result = await runner.run(triage_agent, request)
return result
# Example customer inquiries
billing_inquiry = (
"I was charged twice for my subscription last month. Can I get a refund?"
)
technical_inquiry = (
"The app keeps crashing when I try to upload photos. How can I fix this? Give me the shortest solution possible."
)
general_inquiry = "What are your business hours?"
# Process the different types of inquiries
billing_response = await handle_customer_request(billing_inquiry)
print(f"Billing inquiry response:\n{billing_response.final_output}\n")
technical_response = await handle_customer_request(technical_inquiry)
print(f"Technical inquiry response:\n{technical_response.final_output}\n")
general_response = await handle_customer_request(general_inquiry)
print(f"General inquiry response:\n{general_response.final_output}")
When the triage agent receives a billing question, it will recognize that the Billing Agent is better suited to handle it and will invoke a handoff. Technical questions will be handed off to the technical agent. It will answer general questions within its capabilities directly. Here is the output:
Billing inquiry response:
I can help with that. Could you please provide the transaction details or the date of the charges? This will help me locate the duplicate charge and process a refund for you.
Technical inquiry response:
Try these steps:
1. Restart the app.
2. Update to the latest app version.
3. Clear the app cache.
4. Restart your device.
If it persists, reinstall the app.
General inquiry response:
Our business hours are Monday to Friday, 9 AM to 5 PM. If you need assistance outside these hours, feel free to reach out and we'll get back to you as soon as possible. Is there anything else I can help you with?
Customizing handoffs
For more control over handoffs, you can use the handoff()
function instead of passing agents directly to the handoffs
parameter:
from agents import Agent, handoff, RunContextWrapper
from datetime import datetime
# Create an agent that handles account-related questions
account_agent = Agent(
name="Account Management",
instructions="""You help customers with account-related issues such as
password resets, account settings, and profile updates.""",
)
# Custom handoff callback function
async def log_account_handoff(ctx: RunContextWrapper[None]):
print(
f"[LOG] Account handoff triggered at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
# In a real app, you might log to a database or alert a human supervisor
# Create a triage agent with customized handoffs
enhanced_triage_agent = Agent(
name="Enhanced Customer Service",
instructions="""You are the initial customer service contact who directs
customers to the right specialist.
If the customer has billing or payment questions, hand off to the Billing Agent.
If the customer has technical problems, hand off to the Technical Agent.
If the customer needs to change account settings, hand off to the Account Management agent.
For general inquiries, you can answer directly.""",
handoffs=[
billing_agent, # Basic handoff
handoff( # Customized handoff
agent=account_agent,
on_handoff=log_account_handoff, # Callback function
tool_name_override="escalate_to_account_team", # Custom tool name
tool_description_override="Transfer the customer to the account management team for help with account settings, password resets, etc.",
),
technical_agent, # Basic handoff
],
)
result = await Runner.run(
enhanced_triage_agent, "I need to change my password."
)
Output:
[LOG] Account handoff triggered at 2025-03-16 17:45:48
The handoff()
function allows you to:
- Specify a custom callback with
on_handoff
- Override the default tool name (normally “transfer_to_[agent_name]”)
- Provide a custom tool description
- Configure input handling (more on this below)
Passing data during handoffs
Sometimes, you want the first agent to provide additional context or metadata when handing off to another agent. The Agents SDK supports this through the input_type
parameter:
from pydantic import BaseModel
from typing import Optional
from agents import Agent, handoff, RunContextWrapper
# Define the data structure to pass during handoff
class EscalationData(BaseModel):
reason: str
priority: Optional[str]
customer_tier: Optional[str]
# Handoff callback that processes the escalation data
async def process_escalation(ctx: RunContextWrapper, input_data: EscalationData):
print(f"[ESCALATION] Reason: {input_data.reason}")
print(f"[ESCALATION] Priority: {input_data.priority}")
print(f"[ESCALATION] Customer tier: {input_data.customer_tier}")
# You might use this data to prioritize responses, alert human agents, etc.
# Create an escalation agent
escalation_agent = Agent(
name="Escalation Agent",
instructions="""You handle complex or sensitive customer issues that require
special attention. Always address the customer's concerns with extra care and detail.""",
)
# Create a service agent that can escalate with context
service_agent = Agent(
name="Service Agent",
instructions="""You are a customer service agent who handles general inquiries.
For complex issues, escalate to the Escalation Agent and provide:
- The reason for escalation
- Priority level (Low, Normal, High, Urgent)
- Customer tier if mentioned (Standard, Premium, VIP)""",
handoffs=[
handoff(
agent=escalation_agent,
on_handoff=process_escalation,
input_type=EscalationData,
)
],
)
With this setup, when the service agent decides to hand off to the escalation agent, it will provide structured data about why the escalation is happening. The system will validate this data against the EscalationData
model before passing it to the process_escalation
callback.
When to use handoffs vs. agent-as-tool
The Agents SDK offers two ways for agents to work together: handoffs and using agents as tools (as we saw in the previous section). Here’s when to use each approach:
Use handoffs when:
- You want to completely transfer control to another agent
- The conversation needs to continue with a specialist
- You’re building a workflow where different agents handle different stages
Use agents-as-tools when:
- The primary agent needs to consult a specialist but maintain control
- You want to incorporate a specialist’s response as part of a larger answer
- You’re building a hierarchical system where a coordinator delegates subtasks
Both approaches can be combined in sophisticated systems, with a main agent that sometimes consults specialists (using them as tools) and sometimes hands off control completely when appropriate.
Handoffs provide a powerful mechanism for building complex agent systems where responsibility transitions between different specialists. When designing your agent architecture, consider which approach best suits your specific use case to create the most effective user experience.
Conclusion and Next Steps
We’ve explored the core components of the OpenAI Agents SDK, from creating basic agents to implementing function tools and managing handoffs between specialist agents. We’ve seen how structured outputs using Pydantic models can make our applications more reliable and maintainable, and how to design agent systems that can handle complex tasks through delegation and specialization.
However, we’ve only scratched the surface of what’s possible with this powerful framework. For developers ready to take their agent systems to the next level, several advanced topics await exploration: streaming responses for real-time updates, tracing and observability for debugging, multi-agent orchestration for complex workflows, context management for maintaining conversation state, and guardrails for ensuring safe and appropriate agent behavior.
As AI agents become increasingly central to modern applications, the skills you’ve developed through this tutorial provide a solid foundation for creating sophisticated AI systems. Continue your learning journey with resources like DataCamp’s guide to learning AI, and remember that effective agent design is as much an art as it is a science — requiring iteration, testing, and a deep understanding of both user needs and AI capabilities.
OpenAI Agents SDK FAQs
What is the OpenAI Agents SDK?
The OpenAI Agents SDK is a Python framework that enables developers to build AI applications capable of making decisions and taking actions. It combines large language models with tools and coordination capabilities, allowing you to create systems that can solve complex problems autonomously rather than simply responding to queries.
What types of tools can agents use in the SDK?
The SDK supports three main types of tools: hosted tools (like WebSearchTool that run on OpenAI's servers), function tools (custom Python functions that extend agent capabilities), and agents-as-tools (using specialized agents as tools for other agents). These allow agents to perform actions like searching the web, accessing APIs, or delegating to specialist agents.
How do structured outputs work in the Agents SDK?
Structured outputs use Pydantic models to define exactly what data structure you want your agent to return. By setting the output_type parameter when creating an agent, you receive properly formatted data objects instead of free-form text. This makes applications more reliable and eliminates the need for manual parsing of agent responses.
What's the difference between handoffs and agents-as-tools?
Handoffs completely transfer control to another agent, making them ideal when the conversation needs to continue with a specialist. Agents-as-tools allow a primary agent to consult a specialist while maintaining control, incorporating the specialist's response into a larger answer. Handoffs are best for workflow transitions, while agents-as-tools work better for hierarchical systems.
What advanced features are available in the OpenAI Agents SDK?
Beyond the core functionality, the SDK offers streaming for real-time updates, tracing for debugging and observability, multi-agent orchestration for complex workflows, context management for maintaining conversation state, and guardrails for ensuring safe agent behavior. These features help developers build sophisticated, production-ready agent systems.

I am a data science content creator with over 2 years of experience and one of the largest followings on Medium. I like to write detailed articles on AI and ML with a bit of a sarcastıc style because you've got to do something to make them a bit less dull. I have produced over 130 articles and a DataCamp course to boot, with another one in the makıng. My content has been seen by over 5 million pairs of eyes, 20k of whom became followers on both Medium and LinkedIn.
Top DataCamp Courses
Course
Working with the OpenAI API
Course
Developing AI Systems with the OpenAI API
blog
Understanding AI Agents: The Future of Autonomous Systems

Vinod Chugani
9 min

Tutorial
A Beginner's Guide to The OpenAI API: Hands-On Tutorial and Best Practices

Tutorial
OpenAI Assistants API Tutorial

Tutorial
OpenAI Realtime API: A Guide With Examples

François Aubry
15 min

Tutorial
OpenAI o1-preview Tutorial: Building a Machine Learning Project
Tutorial