Generative AIBuilding with foundation modelsTools & frameworksOpenAI Agents SDK

Building agents with the OpenAI Agents SDK

13 minutes read

The OpenAI Agents SDK takes traditional function calling patterns and elevates them into a framework for building agentic applications. Rather than wiring everything manually, the SDK provides structured building blocks to create multi-agent workflows with less boilerplate code. In this topic, we'll build a coding assistant using these core building blocks.

The agent loop

An agent processes input, decides whether to call a tool or hand off to another agent, and repeats this process until the task is complete. Agents use a request-execution-response loop, much like function calling. However, function calling requires significant manual work, including managing conversation loops, tool dispatch, and state management. When using the Agents SDK, all this is streamlined.

To use the SDK, you need the openai-agents library:

pip install openai-agents

Also, remember to have your OpenAI API key set as an environment variable (OPENAI_API_KEY) or load it from your .env file. Once that is set up, you can create an agent with only a few lines of code:

from agents import Agent, Runner
from dotenv import load_dotenv

load_dotenv()

planner_agent = Agent(
    name="Planner", # the agent's name
    model="gpt-5.4-mini", # the model to use
    instructions="""You are a planning assistant. Given a coding task, break it down into a clear, numbered step-by-step plan. 
    Only output the plan, do not write code.""", # high-level instructions
)

result = Runner.run_sync(planner_agent, "Build a Python CLI calculator") # agent invocation
print(result.final_output)

Calling Runner.run_sync() starts the agent loop:

  1. Send instructions and input to the model.

  2. Inspect the model’s response. If it contains tool calls, execute them and loop back to step 1 with the results. If it contains a handoff, switch to the target agent and loop. If it’s a final text response, stop.

  3. Return the completed RunResult containing the final output, all intermediate steps, and metadata.

This loop replaces manual orchestration—the SDK now handles message accumulation, tool dispatch, and multi-turn execution for you. For asynchronous applications, use Runner.run() instead:

import asyncio
from agents import Agent, Runner
from dotenv import load_dotenv

load_dotenv()

async def main():
    result = await Runner.run(planner_agent, "Build a Python CLI calculator")
    print(result.final_output)

asyncio.run(main())

Runner.run_streamed() streams output to users to reduce perceived latency.

Tools

The SDK supports three categories of tools:

  • Function tools you define;

  • Hosted tools that run on OpenAI’s servers (such as code interpreter and web search);

  • Tools served via MCP.

To define function tools, decorate your Python functions with @function_tool to make them available to an agent. The SDK automatically generates the JSON schema for the tool from your function’s type hints and docstring.

from agents import Agent, Runner, function_tool
from dotenv import load_dotenv

load_dotenv()

@function_tool
def write_file(filename: str, content: str) -> str:
    """Write content to a file and return confirmation."""
    with open(filename, "w") as f:
        f.write(content)
    return f"Successfully wrote {len(content)} characters to {filename}"

coder_agent = Agent(
    name="Coder",
    model="gpt-5.4-mini",
    instructions="""You are a Python developer. Write clean, well-documented code.
    Use the write_file tool to save your code to files.""",
    tools=[write_file],
)

result = Runner.run_sync(coder_agent, "Write a function that checks if a number is prime and save it to prime.py")
print(result.final_output)

When the model calls write_file the SDK parses the arguments, runs your Python function, and returns the result to the conversation. For hosted tools, import the tool and equip the agent with it:

from agents import Agent, WebSearchTool # import the web search tool
from dotenv import load_dotenv

load_dotenv()

agent = Agent(
    name="Coder",
    model="gpt-5.4-mini",
    tools=[WebSearchTool()], # allows the model to search the web
)

MCP tools

For MCP tools, the SDK provides various connection options depending on where and how your MCP server runs. When your MCP server is accessible over the internet, you can just hand the URL directly to the SDK. The model reaches the server on its own—no local proxy or process needed. For this, wrap the endpoint in HostedMCPTool in your tools configuration:

from agents import Agent, HostedMCPTool 

agent = Agent(
    name="HyperAssistant",
    tools=[
        HostedMCPTool(
            tool_config={
                "type": "mcp",
                "server_label": "my_hosted_server",
                "server_url": "<server_url>",
                "require_approval": "never",
            }
        )
    ],
)

For servers that use the Streamable HTTP transport protocol, both local and remote, use MCPServerStreamableHttp. The SDK manages the HTTP connection lifecycle and tool calls.

from agents.mcp import MCPServerStreamableHttp
from agents import Agent

async with MCPServerStreamableHttp(
    name="<server_name>",
    params={
        "url": "<server_url>",
        "headers": {"Authorization": f"Bearer <your_token>"},
        "timeout": 10,
    },
    max_retry_attempts=3,
) as server:
    agent = Agent(
        name="My assistant",
        instructions="Use the MCP tools to answer the questions.",
        mcp_servers=[server],
    )

At the start of each run, the SDK queries the MCP server for available tools and presents them to the model. You can combine MCP tools and function tools on the same agent. For our coding assistant, we'll use MCPServerStdio, which is another method for communicating with local servers over stdin/stdout. This is ideal for CLI-based servers, such as the filesystem server, which will allow our assistant to work with files in the project directory:

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

from dotenv import load_dotenv
load_dotenv()

project_dir = Path(__file__).parent

async def main():
    async with MCPServerStdio(
        cache_tools_list=True,
        params={
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-filesystem", str(project_dir)],
        },
    ) as mcp_server:
        agent = Agent(
            name="Coder",
            model="gpt-5.4-mini",
            instructions="""
            You're a helpful assistant for coding tasks. You have access to tools for accessing the file system. Use them to read and write files as needed to complete the user's requests.
            For the latest information, use the web search tool.
            """,
            mcp_servers=[mcp_server],
            tools=[WebSearchTool()],
        )
        result = await Runner.run(agent, "Fetch the content of agent.py change it to use MCPServerSSE with Docker MCP Gateway.")
        print(result.agent_tool_invocation)
        print(result.final_output)

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

Each MCP server is configured as an async context manager (async with) so the connection is cleanly opened and closed. You can pass one or more servers via the mcp_servers parameter to an Agent. The SDK will automatically fetch the server's tool list, route model calls to the right server, and return results to the conversation. Setting cache_tools_list=True avoids re-fetching the tool list on every agent run, especially when the server's available tools don't change between calls.

Multi-agent systems

With the Agents SDK, you can build multi-agent systems that follow these patterns:

  • Manager agent invoking specialized subagents;

  • Handoffs, where agents hand off control to their peers.

In the first approach, a central orchestrator calls specialized agents, which you define as tools. For example, if we had three agents:

from agents import Agent

planner_agent = Agent(
    # agent definition
)
coder_agent = Agent(
    # agent definition
)

coding_assistant = Agent(
    name="Coding assistant", 
    instructions="""
    When given a coding task, call the relevant tools to create plans and write code. 
    """
    tools=[
        planner_agent.as_tool(
            tool_name="planner", 
            tool_description="Generates plans given a coding task.",
        ),
        coder_agent.as_tool(
            tool_name="coder", 
            tool_description="Generates code given a list of requirements.",
        ),
    ],
)

The key mechanism here is the .as_tool() function, which turns an agent into a tool that the orchestator can call. The next mechanism is handoffs. A handoff tells the SDK: "When this agent determines it should, transfer control to another agent." The receiving agent takes over the conversation with its own instructions, tools, and capabilities.

This is where our coding assistant comes together. The planner breaks down the task, hands it off to the coder, and the coder hands it off to the reviewer. The workflow moves from Planner to Coder to Reviewer. If the reviewer finds issues, it loops back to the Coder and then returns to the Reviewer. The agent loop manages this process automatically. Each handoff includes the full conversation history, so the receiving agent has all the needed context.

Full code
from agents import Agent, Runner
from agents.mcp import MCPServerStdio
from agents import WebSearchTool
from pathlib import Path
import asyncio

from dotenv import load_dotenv
load_dotenv()

project_dir = Path(__file__).parent

async def main():
    planner_agent = Agent(
        name="Planner",
        model="gpt-5.4-mini",
        instructions="""
        You are a planning assistant. Break the coding task into a numbered step-by-step plan, then hand off to the Coder to implement it.
        """,
    )

    reviewer_agent = Agent(
        name="Reviewer",
        model="gpt-5.4-mini",
        instructions="""
        You are a code reviewer. Analyze the code for correctness, style, and edge cases. 
        If the code is acceptable, provide a final summary of what was built. 
        If it needs changes, hand off back to the Coder with specific revision instructions.
        """,
    )

    async with MCPServerStdio(
        cache_tools_list=True,
        params={
            "command": "npx",
            "args": ["-y", "@modelcontextprotocol/server-filesystem", str(project_dir)],
        },
    ) as mcp_server:
        coder_agent = Agent(
            name="Coder",
            model="gpt-5.4-mini",
            instructions="""
            You're a helpful assistant for coding tasks. You have access to tools for accessing the file system. 
            Use them to read and write files as needed to complete the user's requests.
            For the latest information, use the web search tool.
            """,
            mcp_servers=[mcp_server],
            tools=[WebSearchTool()],
            handoffs=[reviewer_agent],
        )

        # Planner hands off to Coder to implement the plan
        planner_agent.handoffs = [coder_agent]

        # Reviewer can hand back to Coder for revisions
        reviewer_agent.handoffs = [coder_agent]

        result = await Runner.run(planner_agent, "Build a Python function that validates email addresses. Save the result to email.py.")
        print(result.final_output)

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

You can also customize handoff behavior with the handoff() function for more control, such as adding input filters or overriding the handoff description the model sees:

from agents import Agent, handoff
from dotenv import load_dotenv

load_dotenv()

coder_agent = Agent(
    name="Coder",
    model="gpt-5.4-mini",
    instructions="You are a Python developer.",
    handoffs=[
        handoff(
            agent=reviewer_agent,
            description="Hand off to the reviewer when code is complete and ready for review.",
        )
    ],
)

Tracing

The Agents SDK comes with tracing out of the box, recording every step of an agent run: model calls, tool executions, handoffs, and guardrail checks. By default, traces are sent to your dashboard, where you can view a timeline of each run:

Agent execution traces in OpenAI dashboard.

Every call to Runner.run_sync() or Runner.run() generates a trace. You can customize the trace name and add metadata as needed:

from agents import Agent, Runner

result = Runner.run_sync(
    planner_agent,
    "Build an email validator",
    run_config={"trace_name": "email-validator-task", "trace_metadata": {"user_id": "hyper-user"}},
)

For custom operations that should appear in the trace, use the trace() and custom_span() context managers:

from agents.tracing import trace, custom_span

with trace("coding-assistant-workflow"):
    with custom_span("planning-phase"):
        plan_result = Runner.run_sync(planner_agent, task)
    
    with custom_span("coding-phase"):
        code_result = Runner.run_sync(coder_agent, plan_result.final_output)
    
    with custom_span("review-phase"):
        review_result = Runner.run_sync(reviewer_agent, code_result.final_output)

Each span appears as a nested segment in the trace timeline. This makes it easier to debug latency, spot failures, and see which agent contributed each part. To send traces to a different backend, implement a custom TracingProcessor and register it with the SDK (for example, in production environments where you handle observability differently). You can also disable tracing entirely:

from agents import Runner

result = Runner.run_sync(
    planner_agent,
    "Build an email validator",
    run_config={"tracing_disabled": True},
)

Conclusion

The OpenAI Agents SDK turns manual function-calling patterns into a declarative, composable framework. Here is what we have covered:

  • How the agent loop works.

  • Using tools.

  • Building multi-agent systems.

  • Tracing.

Using these building blocks, you can create systems that are more capable and easier to maintain. The coding assistant here is just a starting point. You can apply the same patterns to customer support workflows, data pipelines, research agents, and any area that needs intelligent orchestration of tools.

How did you like the theory?
Report a typo