Ad space · Your tool here
PluginStack
Tutorial

OpenAI Agents SDK tutorial for Python

Checked against the official docs:

The OpenAI Agents SDK (openai-agents on PyPI) is OpenAI's open-source Python framework for building agents: a model with instructions, tools it can call, and the option to hand a conversation to another agent. This tutorial goes from an empty folder to an agent that uses your own Python functions, routes questions between specialists, remembers earlier turns and calls an MCP server. Every snippet follows the SDK's official docs, and the imports were checked against openai-agents 0.24.0 (the current release on 4 November 2026).

Quick answer

  1. Use Python 3.10 or newer. Make a virtual environment and run pip install openai-agents.
  2. Set OPENAI_API_KEY in your environment.
  3. Create an Agent(name=..., instructions=...) and run it with Runner.run(agent, "your question") inside asyncio.run(...), or Runner.run_sync(...) in a plain script.
  4. Read the answer from result.final_output.

Free: MCP Starter Checklist

One page: Inspector smoke-test, Claude Code + Cursor connect steps, and the stdout gotcha. Join the list and download it free.

Markdown download + occasional PluginStack updates. No spam. Privacy policy.

1. Install the SDK

The package needs Python 3.10 or newer. Make a project folder with its own virtual environment so the SDK and its dependencies stay out of your system Python:

bash
mkdir my_project
cd my_project
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install openai-agents

If you use uv, uv init and then uv add openai-agents does the same thing. The package is called openai-agents, but you import it as agents. Don't install the agents package from PyPI by mistake: that is a different project (TensorFlow Agents).

2. Set your API key

The SDK reads your key from the OPENAI_API_KEY environment variable. Create a key in the OpenAI dashboard, then set it in the terminal you run the script from:

macOS / Linux
bash
export OPENAI_API_KEY=sk-...
Windows PowerShell
powershell
$env:OPENAI_API_KEY = "sk-..."

That only lasts for the current terminal session. Keep the key out of your code and out of git. One option is a .env file that you load yourself and list in .gitignore.

3. Your first agent

An agent is a model plus instructions. Runner runs the loop: it calls the model, runs any tools the model asks for, and stops when the model gives a final answer. Save this as main.py:

main.py
python
import asyncio
from agents import Agent, Runner

agent = Agent(
    name="History Tutor",
    instructions="You answer history questions clearly and concisely.",
)

async def main():
    result = await Runner.run(agent, "When did the Roman Empire fall?")
    print(result.final_output)

if __name__ == "__main__":
    asyncio.run(main())
bash
python main.py

No model is set here, so the SDK uses its default model. To choose one, pass model="..." to Agent, or set OPENAI_DEFAULT_MODEL for every agent that doesn't name one. The models page in the SDK docs lists the current default.

run, run_sync or run_streamed?

  • await Runner.run(...) is async. Use it in async code, web servers and anything that runs several agents at once.
  • Runner.run_sync(...) wraps the same thing for plain scripts. It won't work inside an event loop that is already running, such as a Jupyter notebook. Use await Runner.run(...) there.
  • Runner.run_streamed(...) returns straight away and gives you events as they happen, so you can print text as it is written.
Streaming text as it arrives (inside an async function)
python
from openai.types.responses import ResponseTextDeltaEvent

result = Runner.run_streamed(agent, input="Tell me three facts about Rome.")
async for event in result.stream_events():
    if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
        print(event.data.delta, end="", flush=True)

4. Give the agent tools

A function tool is an ordinary Python function the model can decide to call. The SDK builds the tool's schema from the function name, the type hints and the docstring, so write the docstring for the model: say what the tool does and when to use it.

python
import asyncio
from agents import Agent, Runner
from agents.decorators import tool

@tool
def get_order_status(order_id: str) -> str:
    """Look up the shipping status of an order by its ID."""
    # Replace this with a real database or API call.
    return f"Order {order_id} shipped yesterday."

agent = Agent(
    name="Support Agent",
    instructions="Help customers with their orders. Use the tools to look things up.",
    tools=[get_order_status],
)

async def main():
    result = await Runner.run(agent, "Where is order 1042?")
    print(result.final_output)

asyncio.run(main())

The quickstart imports tool from agents.decorators. In 0.24.0, tool is an alias for function_tool, so code that uses from agents import function_tool and @function_tool works the same way.

The SDK also has hosted tools that run on OpenAI's side, such as WebSearchTool() and FileSearchTool(...), which you add to tools the same way. The docs list them as built-in tools for OpenAI's Responses models.

A tool can do anything your Python code can do. If it deletes, sends or pays for something, check the inputs inside the function and think about requiring a human approval step. The SDK's human-in-the-loop guide shows how to pause a run for approval.

5. Hand off between agents

A handoff lets one agent pass the conversation to another that is better suited to it. Give each specialist a handoff_description so the routing agent knows when to use it:

python
from agents import Agent, Runner

history_tutor = Agent(
    name="History Tutor",
    handoff_description="Specialist agent for historical questions",
    instructions="You answer history questions clearly and concisely.",
)

math_tutor = Agent(
    name="Math Tutor",
    handoff_description="Specialist agent for math questions",
    instructions="You explain math step by step.",
)

triage_agent = Agent(
    name="Triage Agent",
    instructions="Route each homework question to the right specialist.",
    handoffs=[history_tutor, math_tutor],
)

result = Runner.run_sync(triage_agent, "What is 15% of 80?")
print(result.last_agent.name)   # which agent answered
print(result.final_output)

After a handoff, the specialist owns the rest of the conversation. If you want a manager agent that stays in charge and calls specialists like functions, use agents as tools instead:

python
manager = Agent(
    name="Manager",
    instructions="Answer the question. Use the tutors when you need them.",
    tools=[
        history_tutor.as_tool(tool_name="history_tutor", tool_description="Answers history questions"),
        math_tutor.as_tool(tool_name="math_tutor", tool_description="Solves math problems"),
    ],
)

6. Get structured output

Set output_type to a Pydantic model and final_output becomes an instance of it rather than plain text. That is much easier to use in code than parsing a sentence:

python
from pydantic import BaseModel
from agents import Agent, Runner

class Ticket(BaseModel):
    category: str
    urgent: bool
    summary: str

classifier = Agent(
    name="Ticket Classifier",
    instructions="Classify the support message.",
    output_type=Ticket,
)

result = Runner.run_sync(classifier, "My card was charged twice and I need it fixed today.")
ticket = result.final_output   # a Ticket object
print(ticket.category, ticket.urgent)

7. Remember earlier turns

Each Runner.run call starts fresh unless you pass the history back in. The simplest way is a session, which stores the conversation and adds it to the next run for you:

python
from agents import Agent, Runner, SQLiteSession

agent = Agent(name="Assistant", instructions="Reply very concisely.")
session = SQLiteSession("conversation_123")   # in memory
# session = SQLiteSession("user_123", "conversations.db")   # saved to a file

result = Runner.run_sync(agent, "What city is the Golden Gate Bridge in?", session=session)
print(result.final_output)

result = Runner.run_sync(agent, "What state is it in?", session=session)
print(result.final_output)   # knows "it" means the bridge

Use one session ID per conversation (per user, per chat thread). Without a database file the history is lost when the program exits. If you want to manage history yourself, pass result.to_input_list() plus the new message into the next run. You can also let OpenAI keep the state with previous_response_id or conversation_id. The SDK's running-agents guide compares the options.

8. Connect an MCP server

MCP (Model Context Protocol) servers give an agent ready-made tools. The SDK can start a local server and expose its tools to the agent. This example uses the official Filesystem MCP server, limited to one folder. It needs Node.js 24 or newer for npx:

python
import asyncio
from pathlib import Path
from agents import Agent, Runner
from agents.mcp import MCPServerStdio

notes_dir = Path(__file__).parent / "notes"

async def main():
    async with MCPServerStdio(
        name="Filesystem",
        params={
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-filesystem", str(notes_dir)],
        },
        client_session_timeout_seconds=30,
    ) as server:
        agent = Agent(
            name="Notes Assistant",
            instructions="Answer questions using the files in the notes folder.",
            mcp_servers=[server],
        )
        result = await Runner.run(agent, "List the files you can see and summarize the newest one.")
        print(result.final_output)

asyncio.run(main())

client_session_timeout_seconds defaults to 5 seconds. The example raises it to 30, as the example in the SDK's MCP docs does. npx -y may download the package before the server starts, so the SDK docs recommend installing the package before connecting. One way is npm install -g @modelcontextprotocol/server-filesystem and then "command": "mcp-server-filesystem" with just the folder in args. To give the agent only some of a server's tools, pass tool_filter=create_static_tool_filter(allowed_tool_names=[...]) (import it from agents.mcp). For Filesystem, ["read_text_file", "list_directory", "search_files"] makes it read-only. The SDK docs' example uses read_file. That name still works, but the server marks it deprecated in favor of read_text_file. More on this server: Filesystem MCP server setup.

For a server that runs on the web, use MCPServerStreamableHttp(name=..., params={"url": "https://...", "headers": {...}}) the same way. With OpenAI's Responses models there is also HostedMCPTool, where the Responses API connects to a remote MCP server and your code never connects to it. Only connect servers you trust: whatever a tool returns goes straight into the model's context.

9. See what happened with tracing

Tracing is on by default. Each run records the model calls, tool calls and handoffs, and you can look through them in the OpenAI Traces dashboard. That shows you why an agent picked a tool or where it got stuck.

To switch it off, set OPENAI_AGENTS_DISABLE_TRACING=1 in the environment, or call set_tracing_disabled(True) (from agents) at the start of your program. Tracing is unavailable for organizations that use OpenAI's APIs under a Zero Data Retention (ZDR) policy.

Common errors

  • `ModuleNotFoundError: No module named 'agents'`: the virtual environment isn't active, or you installed into a different Python. Activate .venv and run pip install openai-agents again.
  • `OpenAIError: Missing credentials. Please pass an `api_key`...`: OPENAI_API_KEY isn't set in the terminal that runs the script. Set it again after opening a new terminal.
  • `MaxTurnsExceeded`: the agent kept calling tools without finishing. The default limit is 10 turns. Tighten the instructions or tool descriptions, or pass max_turns= to Runner.run if the task really needs more steps.
  • `RuntimeError: AgentRunner.run_sync() cannot be called when an event loop is already running.`: you called Runner.run_sync inside Jupyter or another async program. Use await Runner.run(...) instead.
  • The MCP server times out on startup: install the package before connecting, as the SDK docs recommend, and check node --version (the Filesystem server has declared Node 24 or newer since v2.0.0).

Where to go next

The SDK repo has runnable examples worth reading next: examples/basic/hello_world.py, examples/basic/tools.py and examples/agent_patterns/routing.py. Guardrails (checks that run on input or output), human approval and voice agents each have their own page in the docs. For a shorter overview of the SDK, how it differs from the regular OpenAI SDK and the JavaScript version, see the OpenAI Agents SDK listing. To give your agent more tools through MCP, the best MCP servers for Claude Code covers popular servers that work with any MCP client.

FAQ

Is the OpenAI Agents SDK free?

The SDK is open source under the MIT license and free to install. You pay for the model calls it makes as normal API usage.

Can I use it with models from other providers?

Yes. The models page in the docs covers non-OpenAI providers, including OpenAI-compatible endpoints and the LiteLLM and Any-LLM adapters (both marked beta). Hosted tools such as web search and file search are built-in tools for OpenAI's Responses models.

What is the difference between the Agents SDK and the OpenAI Python SDK?

The openai package sends single requests to the API. The Agents SDK is built on top of it and adds the agent loop: running tools, handoffs, sessions, guardrails and tracing. The listing has a short comparison.

Is there a JavaScript or TypeScript version?

Yes, @openai/agents on npm, with similar concepts. This tutorial covers the Python package only.

Sources

Commands and config above come from these official docs. Tools change often, so check them for the latest steps.

PluginStack is independent and not affiliated with or endorsed by the companies mentioned. Spotted something out of date? Email pluginstackai@gmail.com.