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-agents

If you're using environment variables (recommended), create a .env file:

OPENAI_API_KEY=your_key_here

Import what you need:

from dotenv import load_dotenv
from agents import Agent, Runner, trace
  • Agent → defines your AI agent

  • Runner → executes your agent

  • trace → 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 role

  • model → 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:

  1. Call each writing agent (through tools)

  2. Collect drafts

  3. Judge the best email

  4. Use the send_email tool 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:

  1. 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

  1. 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.

  2. The instructions you give the agent are what determines tool usage.

  3. 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.

  4. 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:

  1. Sales Manager first generates drafts using the draft-generating tools.

  2. It then selects the best draft using its LLM reasoning (this happens internally in the agent — not via a tool).

  3. Only the winning draft is passed as input to the Email Manager agent.

  4. 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

  1. Automation: You can feed the output directly into an email-sending pipeline.

  2. Evaluation: You can programmatically check subject lines, tones, or body length.

  3. Multi-agent pipelines: Downstream agents (like Email Manager) can use each field separately for formatting or sending.

E. Guardrails

  1. 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: str

Step 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 name
  1. Output 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: bool

Step 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:

  • TResponseInputItem is 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 TResponseInputItem objects, 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 (user or assistant)

  • content → the actual text

  • Optionally, 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.

Reply

or to participate

Keep Reading

No posts found