n4nAI

Setting up GPT-5 agents with the OpenAI Agents SDK

Hands-on tutorial for building GPT-5 agents with the OpenAI Agents SDK. Covers setup, tool use, handoffs, and running multi-agent workflows. Step-by-step code included.

n4n Team3 min read729 words

Audio narration

Coming soon — every post will get a voice note here.

The openai agents sdk gpt-5 stack gives you a minimal, code-first way to build multi-agent workflows without a heavy orchestration layer. This tutorial walks from a clean environment to a running triage agent that delegates to a tool-using specialist, all on gpt-5.

Prerequisites

  • Python 3.10 or newer
  • The official openai-agents package
  • An OpenAI API key, or credentials for any OpenAI-compatible endpoint
  • Basic comfort with Python async is helpful but not required

If you have worked with OpenAI’s chat completions API, the mental model transfers directly. The SDK adds agent definitions, tool wiring, and handoff routing on top of the raw client.

Install and authenticate

Install the package from PyPI:

pip install openai-agents

Export your credentials. The SDK reads OPENAI_API_KEY automatically:

export OPENAI_API_KEY="sk-your-key"

If you later route through a gateway, you will instead set OPENAI_BASE_URL. We cover that near the end.

A single agent with gpt-5

An agent is a named instruction set bound to a model. The model parameter takes any OpenAI model id; we pass "gpt-5" to pin the agent to that model.

from agents import Agent, Runner

assistant = Agent(
    name="Assistant",
    instructions="Answer concisely. Use one sentence.",
    model="gpt-5",
)

result = Runner.run_sync(assistant, "What is the capital of France?")
print(result.final_output)

Expected output

Paris.

Runner.run_sync blocks the calling thread and returns a RunResult. The SDK constructs a chat completions request where the agent’s instructions become the system message and the user prompt is the first human message. For a single turn, this is equivalent to a direct API call but keeps the prompt logic colocated with the agent object.

Adding tools

Tools let the model execute your code. Decorate a plain function with @function_tool; the SDK infers the JSON schema from the signature and docstring.

from agents import Agent, Runner, function_tool

@function_tool
def get_weather(city: str) -> str:
    """Return current weather for a city."""
    # Stubbed response; replace with a real HTTP call
    return f"Sunny, 22C in {city}"

weather_agent = Agent(
    name="Weather",
    instructions="Use the get_weather tool for weather questions. Report the result directly.",
    model="gpt-5",
    tools=[get_weather],
)

res = Runner.run_sync(weather_agent, "What's the weather in Berlin?")
print(res.final_output)

Tool execution output

Sunny, 22C in Berlin

The model emits a tool call. The SDK runs get_weather("Berlin"), appends the return value as a tool message, and performs a second model pass to synthesize the final answer. Inspect res.tool_calls to see the exact arguments the model produced. Keep tool latency low; it directly adds to user-perceived latency.

Multi-agent handoffs

Real systems route work. The openai agents sdk gpt-5 handoff pattern lets one agent yield control to another by referencing it in the handoffs list. The router does not answer; it selects a specialist.

triage = Agent(
    name="Triage",
    instructions=(
        "You are a router. If the user asks about weather, hand off to Weather. "
        "Otherwise hand off to Assistant."
    ),
    model="gpt-5",
    handoffs=[weather_agent, assistant],
)

result = Runner.run_sync(triage, "Tell me about the weather in Lisbon.")
print(result.final_output)

Running the triage flow

Expected console output:

Sunny, 22C in Lisbon

The triage agent returns a handoff message instead of text. Runner detects the target agent, swaps the active instruction set, and continues the conversation loop with the original user input. You can build deep graphs, but keep them acyclic—loops between handoffs will hit max_turns and raise.

Inspecting the run transcript

Debugging agent drift starts with the raw message list. Every RunResult exposes messages:

result = Runner.run_sync(triage, "What is 2+2?")
for msg in result.messages:
    print(msg.role, ":", str(msg.content)[:80])

This prints system, user, assistant (handoff), and final assistant messages. When a handoff fails to trigger, you will see the router attempting to answer directly. Adjust instructions until the first assistant message is a handoff.

Async and streaming

Production services should use async. The SDK provides Runner.run (coroutine) and Runner.run_streamed for token streaming.

import asyncio
from agents import Runner

async def main():
    stream = Runner.run_streamed(assistant, "Name three planets.")
    async for event in stream.stream_events():
        if event.type == "output_text_delta":
            print(event.delta, end="", flush=True)
    print()
    print("FINAL:", stream.result.final_output)

asyncio.run(main())

Streaming cuts time-to-first-token. Consume output_text_delta events for UI updates; the aggregated string is available as stream.result.final_output after the loop completes.

Using a compatible inference gateway

The Agents SDK is a thin OpenAI client wrapper. Point it at any OpenAI-compatible base URL by setting OPENAI_BASE_URL and a matching key. This is useful when you want one endpoint that fronts many models or adds fallback.

For example, n4n.ai exposes a single OpenAI-compatible endpoint covering 240+ models and automatically falls back when a provider is rate-limited or degraded. To use it, change the environment:

export OPENAI_BASE_URL="https://api.n4n.ai/v1"
export OPENAI_API_KEY="your-gateway-key"

No Python changes are required. model="gpt-5" still routes through the gateway, which forwards provider cache-control hints and meters per-token usage. Your existing Runner calls gain multi-provider resilience without code edits.

Adding a guardrail agent

Before returning to the user, you can validate output with a second agent. This pattern catches malformed responses from tools.

guard = Agent(
    name="Guard",
    instructions="Check the input for profanity or unsafe content. Reply 'OK' or 'BLOCK'.",
    model="gpt-5",
)

def moderate(text: str) -> str:
    res = Runner.run_sync(guard, text)
    return text if res.final_output.strip() == "OK" else "[blocked]"

final = moderate(result.final_output)
print(final)

Run the guard synchronously inside your request path, or pipeline it in async if throughput matters.

Production notes

  • Model ids are plain strings. Swapping gpt-5 for a dated snapshot only touches the agent definition.
  • Bound your loops. Pass max_turns to Runner.run to cap agent-tool round trips and control cost.
  • Tool errors. Wrap tool bodies in try/except and return a descriptive string. Unhandled exceptions abort the entire run.
  • Concurrency. Each Runner.run is independent. Use asyncio and a shared HTTP client for high parallelism.
  • Logging. Persist result.messages in your trace store. The transcript is the only ground truth when prompts misbehave.

The openai agents sdk gpt-5 setup above is enough to ship a routed assistant with tool use and guardrails. From here, add input schema validation, per-user context injection, and metric hooks around Runner to monitor token spend per agent.

Tagsgpt-5openai-agents-sdktutorialsetup

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All gpt-5 agentic capabilities posts →