✅ What is OpenAI Agents SDK — Quick Overview
The Agents SDK is a lightweight, Python-first framework for building “agentic” AI apps: LLM-powered agents that can use tools, manage state, orchestrate flows, hand off tasks, and more.
Its core primitives are: Agents, Handoffs, Guardrails, and Sessions.
It supports built-in tracing/observability so you can debug and inspect agent executions — great for production debugging or evaluation.
We will deep dive into this framework with examples, to provide you the clear understanding of all the important concepts of Open AI Agents SDK.
What Is an AI Agent System?
An AI Agent System is a set of LLM-powered agents that:
Follow their own instructions
Use tools (functions or APIs)
Collaborate with other agents
Pass control through handoffs when needed
The OpenAI Agent SDK makes this extremely simple: each agent has instructions, tools, and handoffs.
Project Setup:
Before writing any code, install the Agent SDK:
pip install openai-agentsIf you're using environment variables (recommended), create a .env file:
OPENAI_API_KEY=your_key_hereImport what you need:
from dotenv import load_dotenv
from agents import Agent, Runner, traceAgent→ defines your AI agentRunner→ executes your agenttrace→ allows you to track what the agent is doing (helpful for debugging and observability)
Load Your Environment Variables
load_dotenv(override=True)This ensures your API key is available inside the script.
Create Your First Agent
agent = Agent(
name="Joketeller",
instructions="You are a joke teller",
model="gpt-4o-mini"
)name→ a simple identifier (useful for logging and debugging)instructions→ the system prompt that defines the agent’s personality and rolemodel→ the OpenAI model powering the agent
Run the Agent With a Prompt:
with trace("Telling a joke"):
result = await Runner.run(agent, "Tell a joke about demogregon")
print(result.final_output)
trace("Telling a joke")
Helps you see what’s happening behind the scenes.Runner.run(agent, prompt)
Sends your prompt to the agent.result.final_output
Returns the final text response from the agent.
Now, you know how to create a simple agent using openai agent sdk. Now, We will look into build a simple AI Agent System. Before that, let’s understand the few basic concepts:
Agents → Independent LLM workers with roles + instructions.
Tools → Python functions an agent can call to execute real-world actions.
Agents-as-tools → Allows agents to call other agents → collaboration.
Handoffs → Full control is transferred from one agent to another → delegation.
Manager/Planner Agent → Coordinates other agents, picks winners, triggers tools/handoffs.
Now, we will understand all these with examples.
A. Multi-Agent Collaboration (3 Style-Based Agents)
We’ll start by creating three agents, each with different writing styles:
sales_agent1 = Agent(
name="Professional Sales Agent (Formal)",
instructions="You write extremely formal and structured business correspondence. Focus on value, metrics, and risk mitigation.",
model="gpt-4o-mini"
)
sales_agent2 = Agent(
name="Engaging Sales Agent (Storyteller)",
instructions="You write creative cold emails that start with a compelling, relevant anecdote or a surprising statistic to immediately capture attention.",
model="gpt-4o-mini"
)
sales_agent3 = Agent(
name="Busy Sales Agent (Bullet Points Only)",
instructions="You write cold emails that are ONLY 3 short bullet points and a single closing sentence. Be direct and avoid introductory fluff.",
model="gpt-4o-mini"
)These three agents form the creative team.
Each produces a different version of the email.
Generating Emails in Parallel
We can run all agents together using asyncio.gather:
message = "Write a cold sales email"
with trace("Parallel cold emails"):
results = await asyncio.gather(
Runner.run(sales_agent1, message),
Runner.run(sales_agent2, message),
Runner.run(sales_agent3, message),
)
outputs = [result.final_output for result in results]
for output in outputs:
print(output + "\n\n")The result is three different drafts created by three separate agents.
Now, we can add the Agent-as-a-Judge to find the best email from above results.
best_sales_picker = Agent(
name="best_sales_picker",
instructions="Pick the best cold email and return the winning email.",
model="gpt-4o-mini"
)This agent evaluates the drafts and chooses which email should be sent.
message = "Write a cold sales email"
with trace("Select the best sales message"):
results = await asyncio.gather(
Runner.run(sales_agent1, message),
Runner.run(sales_agent2, message),
Runner.run(sales_agent3, message),
)
outputs = [result.final_output for result in results]
emails = "Cold sales emails from all the agents:\n\n" + "\n\nEmail:\n\n".join(outputs)
best_email = await Runner.run(best_sales_picker, emails)
print(f"The best sales email:\n{best_email.final_output}")You can go and checkout the trace here: https://platform.openai.com/traces
B. Adding Tools (Function Calling Made Simple)
Tools allow agents to take actions in the real world.
Here’s a tool that sends an email:
@function_tool
def send_email(body: str):
"""Send an email with the given body"""
...
return {"status": "success"}The decorator @function_tool converts our Python function into a usable tool that agents can call autonomously.
Agents as Tools (Delegation with Reusability)
Agents themselves can be converted into tools:
tool1 = sales_agent1.as_tool(tool_name="sales_agent1", tool_description="Write a cold sales email")
tool2 = sales_agent2.as_tool(tool_name="sales_agent2", tool_description="Write a cold sales email")
tool3 = sales_agent3.as_tool(tool_name="sales_agent3", tool_description="Write a cold sales email")This means agents can dynamically call each other the same way they call functions.
The Manager Agent (AI Planner)
Now we create a Sales Manager agent, whose job is to:
Call each writing agent (through tools)
Collect drafts
Judge the best email
Use the
send_emailtool to send it
instructions = """
You are a Sales Manager. Generate drafts using the sales_agent tools,
pick the best one, then send it using the send_email tool.
"""
sales_manager = Agent(
name="Sales Manager",
instructions=instructions,
tools=[tool1, tool2, tool3, send_email],
model="gpt-4o-mini"
)Everything now runs through the Sales Manager.
Important points to remember for tools:
Tools are NOT called automatically or sequentially. The agent will call only the tools it believes are needed based on:
a. Your instructions
b. The user input
c. The agent’s own reasoning
The order inside the
tools=[...]list does NOT matter. The model doesn’t care about order — it only uses the tool descriptions and instructions to decide which to call.The instructions you give the agent are what determines tool usage.
The agent might use all the tools or fewer tools depending on your instruction and task. If you remove instructions, the agent often won't call any tool.
If you want the agent to use the tool in the specific order, then you need to mention it in the prompt.
C. Handoffs (Passing Full Control to Another Agent)
Tools return control to the calling agent.
Handoffs transfer control entirely to another agent.
Example:
Agent A → hands off → Agent B
Agent B takes over until the task is done.
Let’s understand this with this example. We have sales manager agent that uses above tools, tool1, tool2, tool3 and then uses LLM reasoning to pick the best the best draft or email.
Now, the sales manager passes the winning email to the Email Manager agent using Handoffs. Then, the Email Manager executes the tools- writes subject, convert to html, sends email.
How the handoff works
When the Sales Manager agent “hands off” to the Email Manager:
Sales Manager first generates drafts using the draft-generating tools.
It then selects the best draft using its LLM reasoning (this happens internally in the agent — not via a tool).
Only the winning draft is passed as input to the Email Manager agent.
The Email Manager now takes full control over that draft.
Now, let’s see the full working code:
# -----------------------------
# Imports
# -----------------------------
import os
from typing import Dict
from dotenv import load_dotenv
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Email, To, Content
from agents import Agent, Runner, trace, function_tool
load_dotenv(override=True)
# 1. Subject Writer Agent → Tool
# -----------------------------
subject_instructions = """
You can write a subject for a cold sales email.
You are given a message and you need to write a subject
that is likely to get a response.
"""
subject_writer = Agent(
name="Email subject writer",
instructions=subject_instructions,
model="gpt-4o-mini"
)
# Convert agent → tool
subject_tool = subject_writer.as_tool(
tool_name="subject_writer",
tool_description="Write a subject for a cold sales email"
)Here, we have first defined the subject writer agent and then converted this as the subject tool. Now, lets define the html converter agent and convert it to the html tool.
# 2. HTML Converter Agent → Tool
# -----------------------------
html_instructions = """
You can convert a text email body to an HTML email body.
You are given a plain text or markdown email body and you need to convert it
into clean, readable HTML with simple design.
"""
html_converter = Agent(
name="HTML email body converter",
instructions=html_instructions,
model="gpt-4o-mini"
)
html_tool = html_converter.as_tool(
tool_name="html_converter",
tool_description="Convert a text email body to an HTML email body"
)Now, we will define the email sender function tool, which will be used to send the email.
# 3. Email Sender Function Tool
# -----------------------------
@function_tool
def send_html_email(subject: str, html_body: str) -> Dict[str, str]:
"""Send an HTML email to all prospects."""
sg = SendGridAPIClient(api_key=os.environ.get("SENDGRID_API_KEY"))
from_email = Email("[email protected]") # Change to your verified sender
to_email = To("[email protected]") # Change to your recipient
content = Content("text/html", html_body)
mail = Mail(from_email, to_email, subject, content).get()
sg.client.mail.send.post(request_body=mail)
return {"status": "success"}Now, lets define all the tools in a list, which will be utilized by the emailer agent.
# All tools in a list
email_tools = [subject_tool, html_tool, send_html_email]
# -----------------------------
# 4. Email Manager Agent (Handoff Target)
# -----------------------------
emailer_instructions = """
You are an email formatter and sender.
You receive the body of an email that must be sent.
Here is your process:
1. First, use the subject_writer tool to generate a subject.
2. Then, use the html_converter tool to turn the body into HTML.
3. Finally, use the send_html_email tool to send the email.
"""
emailer_agent = Agent(
name="Email Manager",
instructions=emailer_instructions,
tools=email_tools,
model="gpt-4o-mini",
handoff_description="Convert an email to HTML and send it"
)Now, lets utilize the sales manager agent, that we defined above while explaining the agent as tools to find the best email and we will provide the clear instructions to first generate the three drafts using sales tools (tools=[tool1, tool2, tool3]) and then evaluate and select the best draft or email. And last pass the winning email to the Email Manager via handoff.
# 5. Sales Manager Agent (Main Orchestrator)
# -----------------------------
sales_manager_instructions = """
You are a Sales Manager at ComplAI. Your goal is to find the single best
cold sales email using the sales_agent tools.
Steps:
1. Generate Drafts:
Use all three sales_agent tools to generate three different email drafts.
Do not proceed until all three drafts are ready.
2. Evaluate and Select:
Review the drafts and choose the single best email using your judgment.
You may use the tools multiple times if needed.
3. Handoff for Sending:
Pass ONLY the winning draft to the 'Email Manager' via handoff.
Rules:
- You must use the sales agent tools to generate drafts.
- Do NOT write drafts yourself.
- Only hand off ONE email, never more.
"""
sales_manager = Agent(
name="Sales Manager",
instructions=sales_manager_instructions,
tools=[tool1, tool2, tool3], # the tools that generate drafts
handoffs=[emailer_agent], # Email Manager is the handoff target
model="gpt-4o-mini"
)
# -----------------------------
# 6. Execute Everything
# -----------------------------
message = "Send out a cold sales email addressed to your manager from Brendon"
with trace("Handoff Agents"):
result = await Runner.run(sales_manager, message)
print(result.final_output)
Key takeaway about handoffs and tools
Concept | Behavior |
|---|---|
Handoff | Passes control (and input) from one agent to another. |
Tools | Used by the agent that has them, based on instructions. |
Tool order | List order does not matter; execution order is determined by instructions. |
Output reliability | Handoff relies on the first agent producing a valid output. Tracing helps debug. |
D. Structured Outputs
Structured outputs allow your agent to return predictable, machine-readable outputs, instead of freeform text. You can define a Pydantic model for this.
Structured Outputs Example — Sales Agent
Step 1: Define the Pydantic model
We want the sales agent to generate a subject line, body, and a recommended tone.
from pydantic import BaseModel
class SalesEmailOutput(BaseModel):
subject: str
body: str
tone: str # e.g., "Professional", "Engaging", "Concise"Step 2: Create the Sales Agent with structured output
from agents import Agent, Runner
instructions = """
You are a sales agent for ComplAI, a SaaS company that helps with SOC2 compliance.
Write a cold sales email including:
- A subject line
- The email body
- The tone of the email ("Professional", "Engaging", "Concise")
Return the result in structured format matching SalesEmailOutput.
"""
sales_agent = Agent(
name="Structured Sales Agent",
instructions=instructions,
output_type=SalesEmailOutput, # structured output
model="gpt-4o-mini"
)Step 3: Run the agent
message = "Write a cold email to a CEO introducing our new AI compliance tool"
result = await Runner.run(sales_agent, message)
# Access structured output
email_data = result.final_output
print(email_data.subject)
print(email_data.body)
print(email_data.tone)✅ Output
SalesEmailOutput(
subject="Boost Your SOC2 Compliance with AI-Powered Tools",
body="Dear CEO, ... Our AI-driven platform ensures seamless SOC2 compliance ...",
tone="Professional"
)Why structured outputs are useful
Automation: You can feed the output directly into an email-sending pipeline.
Evaluation: You can programmatically check subject lines, tones, or body length.
Multi-agent pipelines: Downstream agents (like Email Manager) can use each field separately for formatting or sending.
E. Guardrails
Input Guardrail Example
Suppose we want to prevent sending cold emails that include a personal name in the request.
Step 1: Define a structured output for checking names
from pydantic import BaseModel
class NameCheckOutput(BaseModel):
is_name_in_message: bool
name: strStep 2: Create a “Name Check” agent
from agents import Agent
name_check_agent = Agent(
name="Name Check Agent",
instructions="Check if the user request contains a personal name. Return True/False and the name if found.",
output_type=NameCheckOutput,
model="gpt-4o-mini"
)Step 3: Create input guardrail
from agents import input_guardrail, GuardrailFunctionOutput, Runner
@input_guardrail
async def guardrail_against_name(ctx, agent, message):
result = await Runner.run(name_check_agent, message, context=ctx.context)
is_name = result.final_output.is_name_in_message
return GuardrailFunctionOutput(
output_info={"found_name": result.final_output},
tripwire_triggered=is_name # If True, agent execution is blocked
)Step 4: Apply input guardrail to Sales Agent
sales_agent = Agent(
name="Sales Agent",
instructions="Write a professional cold email for ComplAI.",
model="gpt-4o-mini",
input_guardrails=[guardrail_against_name]
)✅ Now, if someone tries:
message = "Send a cold email to Alice about our AI tool"
result = await Runner.run(sales_agent, message)
# Execution will be blocked because the input guardrail detected a nameOutput Guardrail Example
Suppose we want to ensure the email body is not too long and contains a specific keyword like “AI-powered compliance.”
Step 1: Define structured output for validation
class EmailValidationOutput(BaseModel):
is_valid_length: bool
contains_keyword: boolStep 2: Create output guardrail agent
output_guardrail_agent = Agent(
name="Email Output Validator",
instructions="""
Check the generated email.
- Ensure the body is less than 200 words.
- Ensure it contains the phrase 'AI-powered compliance'.
Return structured output with booleans.
""",
output_type=EmailValidationOutput,
model="gpt-4o-mini"
)Step 3: Wrap it in an output guardrail function
from agents import GuardrailFunctionOutput
async def output_guardrail(ctx, agent, message, agent_output):
result = await Runner.run(output_guardrail_agent, agent_output.body)
valid_length = result.final_output.is_valid_length
contains_keyword = result.final_output.contains_keyword
tripwire = not (valid_length and contains_keyword)
return GuardrailFunctionOutput(
output_info={"validation": result.final_output},
tripwire_triggered=tripwire
)Step 4: Apply output guardrail to Sales Agent
sales_agent_with_output_guardrail = Agent(
name="Sales Agent",
instructions="Write a professional cold email for ComplAI.",
model="gpt-4o-mini",
output_guardrails=[output_guardrail]
)✅ Now, after generating the email, the output guardrail checks:
Is the email short enough?
Does it include “AI-powered compliance”?
If not, the agent’s output is flagged, and you can handle it (e.g., regenerate, log, or alert).
Guardrail Type | Purpose | Example |
|---|---|---|
Input Guardrail | Prevent bad or risky inputs | Block requests containing personal names |
Output Guardrail | Ensure output quality & rules | Check email length & required keywords |
Both together | Full safety & compliance | Ensures agents only produce valid, safe, actionable outputs |
F. Different Models
The OpenAI Agents SDK allows you to plug in any model compatible with OpenAI endpoints — not just OpenAI’s own GPT models. In your code, you saw examples using:
DeepSeek (
deepseek-chat)Google Gemini (
gemini-2.0-flash)Llama3.3 (
llama-3.3-70b-versatile)
You can assign different models to different agents, allowing you to compare outputs, experiment, or parallelize different styles of responses.
from agents import Agent, OpenAIChatCompletionsModel
from openai import AsyncOpenAI
# Setup API clients for different endpoints
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
deepseek_client = AsyncOpenAI(base_url=DEEPSEEK_BASE_URL, api_key="YOUR_DEEPSEEK_KEY")
# Wrap in OpenAIChatCompletionsModel
deepseek_model = OpenAIChatCompletionsModel(model="deepseek-chat", openai_client=deepseek_client)
# Assign to an agent
sales_agent = Agent(
name="DeepSeek Sales Agent",
instructions="Write professional cold sales emails",
model=deepseek_model
)You can assign different LLMs to different agents in the same pipeline.
G. Memory
What is TResponseInputItem?
In OpenAI Agents SDK:
TResponseInputItemis essentially a unit of conversation or message in memory.Each item represents a piece of context, usually with a role (
user,assistant,system) and content.Memory stores a list of
TResponseInputItemobjects, which agents can read or append to over time.
from agents.types import TResponseInputItem
item = TResponseInputItem(
role="user",
content="Write a cold email introducing our AI compliance tool."
)role→ who sent the message (userorassistant)content→ the actual textOptionally, you can have metadata like timestamps or tool usage info.
Memory as a collection of TResponseInputItem
When an agent has memory attached, each input/output is stored internally as a TResponseInputItem.
from agents import Memory
# Create an in-memory context
sales_memory = Memory(name="sales_agent_memory", type="in-memory")
# Add a user message
sales_memory.add_message(role="user", content="Send a cold email to the CEO")
# Add an assistant (agent) response
sales_memory.add_message(role="assistant", content="Dear CEO, ... Our AI tool ensures SOC2 compliance.")Internally, these are stored as TResponseInputItem objects:
memory_contents = sales_memory.get_context()
for item in memory_contents:
print(f"{item.role}: {item.content}")Output:
user: Send a cold email to the CEO
assistant: Dear CEO, ... Our AI tool ensures SOC2 compliance.H. Three Classes of Tools
1. Hosted (Built-in) Tools
These are prebuilt tools provided by OpenAI (when using the “official” model endpoint) — i.e. you don’t need to implement them yourself. The SDK exposes them to agents automatically if you include them.
Examples include:
WebSearchTool— lets an agent search the web.FileSearchTool— lets an agent search documents from a vector store or retrieval store.CodeInterpreterTool— lets the LLM execute code in a sandbox (great for calculations, data processing, quick scripting).ComputerTool,LocalShellTool— let agents run shell commands or more complex "computer use" tasks.ImageGenerationTool, tools for image creation or remote MCP‑based tools.
When to use:
Want standard capabilities (web search, file retrieval, code execution) without implementing them from scratch.
Your environment supports the hosted‑tool backend (e.g. official OpenAI endpoints).
from agents import Agent, Runner, WebSearchTool, FileSearchTool
agent = Agent(
name="Researcher",
instructions="Research best programming blogs about data science.",
tools=[ WebSearchTool(), FileSearchTool(vector_store_ids=["my_embeddings"]) ]
)
result = await Runner.run(agent, "Find three top data‑science blogs and summarize their focus.")
print(result.final_output)This lets the agent actually fetch data rather than hallucinate.
2. Function Tools
What they are: Any Python function wrapped as a tool using
@function_tool.Purpose: Allow agents to perform custom actions (send emails, query a database, call APIs).
from agents import function_tool
@function_tool
def send_email(body: str):
# code to send email
return {"status": "sent"}Use case: Integrate your backend systems with the agent.
3. Agents-as-Tools
What they are: Treat one agent as a callable tool for another agent.
Purpose: Create modular sub-agents and orchestrate them from a main agent.
Example:
sub_agent = Agent(name="Summarizer", instructions="Summarize text.")
summary_tool = sub_agent.as_tool(tool_name="summarize_text")Use case: Master agent calls specialized sub-agents for specific tasks.
Tool Type | Action | When to Use |
|---|---|---|
Hosted Tool | Prebuilt API calls (web, file, code) | Standard tasks without coding |
Function Tool | Custom Python logic | Backend integration, APIs, custom actions |
Agents-as-Tool | Sub-agent called like a function | Modular workflows, orchestration |
We have now reached the conclusion of our comprehensive tutorial on the OpenAI Agent SDK Framework.
This detailed guide was designed to provide you with a thorough understanding of all the essential concepts within the framework. We recognize the length of this tutorial may be significant, and we encourage you to engage with the material in manageable sections if preferred.
However, the intentional depth and scope ensure that you have a single, unified resource covering all critical components for a cohesive learning experience and successful implementation.
Thank you for following along! We look forward to seeing the powerful agents you build.

