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-agentspackage - 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-5for a dated snapshot only touches the agent definition. - Bound your loops. Pass
max_turnstoRunner.runto 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.runis independent. Useasyncioand a shared HTTP client for high parallelism. - Logging. Persist
result.messagesin 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.