In 2015, every engineering team decomposed their Rails monolith into microservices and spent the next three years debugging distributed system failures they never had before. The multi-agent AI space is repeating the same mistake. Handoff-based architectures scatter control across autonomous specialists, and teams are rediscovering that distributed coordination is hard—whether the nodes are Docker containers or LLM agents.
The OpenAI Agents SDK (v0.12.5, released March 19, 2026, 20,200+ GitHub stars) quietly encodes the lesson the industry already learned: start with centralized control. This tutorial walks through OpenAI Agents SDK multi-agent workflows using the agents-as-tools pattern—from zero to a running orchestrator in under 100 lines of Python. You’ll build specialist agents, wire them into a central manager, add guardrails, and learn which production pitfalls will bite you first.
Handoffs vs. Agents-as-Tools: Pick the Right OpenAI Agents SDK Multi-Agent Workflows Pattern
The SDK ships two orchestration patterns, and choosing wrong will cost you a rewrite. Handoffs route the conversation to a specialist agent that takes over entirely—the specialist responds directly to the user. Think independent microservices: each agent is fully autonomous. Agents-as-tools keeps a manager agent in control. The manager calls specialists via Agent.as_tool(), collects their outputs, and synthesizes a single response.
The SDK documentation puts the distinction clearly: “Use agents as tools when a specialist should help with a bounded subtask but should not take over the user-facing conversation.” If you need a single coherent response that synthesizes multiple specialist outputs, agents-as-tools is the pattern. If you’re building a routing layer where each specialist owns its conversation (customer support triage, for example), handoffs work. The patterns can also be combined—a triage agent hands off to a specialist that still calls sub-agents as tools for narrow subtasks.
If you’ve worked with graph-based orchestration in LangGraph, the agents-as-tools pattern will feel familiar—centralized control with decomposed execution. The difference: LangGraph makes you define the graph explicitly. Here, the LLM decides which tools to call.
Prerequisites and Setup
You need Python 3.10+, an OpenAI API key with credits, and one install command:
pip install openai-agents
export OPENAI_API_KEY=sk-...
python -c "import agents; print(agents.__version__)"
# Expected: 0.12.5
The entire SDK is built on four primitives: Agent (an LLM with instructions and tools), Runner (executes the agent loop), Tool (anything the agent can call), and Guardrail (validates input or output). That’s it. The official documentation covers each in depth, but these four are all you need for this tutorial. Note that Runner.run() is async; Runner.run_sync() is the synchronous wrapper we’ll use here. The SDK is pre-1.0 (v0.12.5 as of March 2026)—core primitives are stable, but pin your version in requirements.txt.
Building the Agents-as-Tools Orchestrator
The pattern follows four steps: define specialists, expose them as tools, create the orchestrator, and run. Here’s a working example—a research assistant that coordinates a fact-checker and a writer.
Defining Specialists and Wiring the Orchestrator
from agents import Agent, Runner, ModelSettings
# Step 1: Define specialist agents
fact_checker = Agent(
name="FactChecker",
instructions="Verify claims. Return VERIFIED or UNVERIFIED with reasoning.",
model="gpt-5.4-nano", # cheap model for bounded tasks
)
writer = Agent(
name="Writer",
instructions="Write clear, concise prose. Use only verified facts provided to you.",
model="gpt-5.4-mini",
)
# Step 2: Expose specialists as tools
# The orchestrator's LLM reads tool_description to decide when to call each
fact_tool = fact_checker.as_tool(
tool_name="verify_claim",
tool_description="Verify a factual claim. Input: the claim to check.",
max_turns=3, # prevent unbounded loops
)
write_tool = writer.as_tool(
tool_name="draft_text",
tool_description="Write polished prose from verified facts. Input: facts and context.",
max_turns=5,
)
# Step 3: Create orchestrator — tools list, NOT agents list
orchestrator = Agent(
name="ResearchOrchestrator",
instructions="""You coordinate research tasks:
1. Use verify_claim to check facts before including them.
2. Use draft_text to produce the final written output.
Always verify before writing.""",
tools=[fact_tool, write_tool],
model="gpt-5.4-mini",
model_settings=ModelSettings(parallel_tool_calls=True, temperature=0),
)
A critical detail: the orchestrator’s tools list contains the .as_tool() results, not the agents themselves. The tool_description string is what the orchestrator’s LLM reads to decide when to invoke each specialist—make it specific. And always set max_turns explicitly. Omit it, and an unexpected reasoning loop burns tokens until you notice.
Running and Chaining Results
from agents import trace
# Step 4: Run the orchestrator
with trace("ResearchWorkflow"):
result = Runner.run_sync(
orchestrator,
"Research and write a 100-word summary of GPT-5.4's capabilities."
)
print(result.final_output)
# Optional: Chain a synthesizer for cleaner output
synthesizer = Agent(
name="Synthesizer",
instructions="Polish the draft into publication-ready prose.",
model="gpt-5.4-mini",
)
with trace("SynthesisStep"):
polished = Runner.run_sync(synthesizer, result.to_input_list())
print(polished.final_output)
The result.to_input_list() method passes the orchestrator’s full conversation history to the synthesizer—a clean handoff that preserves context without manual state management. With parallel_tool_calls=True, the orchestrator calls multiple specialists simultaneously instead of sequentially, cutting latency for independent subtasks. The official agents-as-tools example shows a minimal translation orchestrator using this exact pattern, and OpenAI’s portfolio collaboration cookbook demonstrates the advanced version with parallel specialist execution.
If you’ve built agents with Anthropic’s framework, you’ll notice the orchestration approach differs significantly—our Claude Agent SDK tutorial covers that alternative in depth.

Guardrails: The Real Reason to Use Agents-as-Tools
Here’s the second-order insight that most tutorials miss. In the agents-as-tools pattern, the orchestrator is always the final agent. That means a single output guardrail on the orchestrator catches problems from any specialist. In a handoff architecture, you’d need to duplicate safety guardrails on every specialist agent independently. For enterprise deployments where compliance is non-negotiable, this centralized chokepoint is the architectural reason to choose agents-as-tools.
The SDK provides three guardrail levels: input (validates the first user message before the orchestrator starts), output (validates the orchestrator’s final response), and tool (per-tool checks on individual function tools via @tool_input_guardrail and @tool_output_guardrail decorators). Input and output guardrails return a GuardrailFunctionOutput with a tripwire_triggered boolean—when triggered, execution halts immediately. Tool guardrails work differently: they use ToolGuardrailFunctionOutput to allow or reject specific tool calls, and only apply to tools created with function_tool.
from agents import GuardrailFunctionOutput, InputGuardrail
async def check_on_topic(ctx, agent, input):
result = await Runner.run(
Agent(
name="TopicGuard",
instructions="Return 'off_topic' if the request isn't about research or writing.",
model="gpt-5.4-nano", # fast, cheap screening
),
input,
context=ctx.context,
)
is_off_topic = "off_topic" in result.final_output.lower()
return GuardrailFunctionOutput(
output_info={"decision": result.final_output},
tripwire_triggered=is_off_topic,
)
# Apply to orchestrator — screens BEFORE any token spend on specialists
orchestrator = Agent(
name="ResearchOrchestrator",
instructions="...",
tools=[fact_tool, write_tool],
input_guardrails=[InputGuardrail(guardrail_function=check_on_topic)],
)
Cost optimization tip: input guardrails run in parallel mode by default. Using GPT-5.4-nano for the guardrail agent adds near-zero latency while the main orchestrator begins processing. If the guardrail trips, execution halts before the expensive specialist calls happen. For more on extending agent capabilities through external tool servers, see our MCP server tutorial.
Cost Optimization and the Five Production Pitfalls
Multi-agent workflows multiply token costs. The fix is straightforward: assign models by task complexity. According to OpenAI’s current pricing, a tiered approach changes the math dramatically:
| Model | Input / 1M tokens | Output / 1M tokens | Best For |
|---|---|---|---|
| GPT-5.4 | $2.50 | $15.00 | Orchestrator (strong reasoning) |
| GPT-5.4-mini | $0.75 | $4.50 | Mid-tier specialists |
| GPT-5.4-nano | $0.20 | $1.25 | Extraction, formatting, guardrails |
A 3-agent workflow averaging 1,000 input and 500 output tokens per agent costs roughly $0.030 per request on all-GPT-5.4. Switch to a mini orchestrator with two nano specialists: ~$0.005 per request—about 6x cheaper. At 1,000 requests per day, that’s ~$900/month versus ~$140/month. Cached input tokens get a 90% discount, so repeated system prompts across tool calls approach free.
Now the pitfalls that will bite you in production:
- Unbounded token accumulation. Each sub-agent call appends to the orchestrator’s context window. Set
max_turnsexplicitly on everyAgent.as_tool()call. Omit it, and an unexpected loop burns your budget overnight. - Session persistence requires explicit setup. The SDK ships built-in Sessions with backends from SQLite (development) to Redis and Dapr (production). Without a session wired up, the orchestrator starts fresh each run. Use
conversation_idfor OpenAI server-managed history. - Debugging nested agents is hard. Nested agent calls don’t surface errors clearly in standard output. Wrap execution in
with trace("Workflow"):and use the OpenAI Traces dashboard. Traces export to 20+ backends including LangSmith and Weights & Biases. - Vendor lock-in is real. The SDK supports 100+ LLMs, but guardrail tripwires and the Traces dashboard only work with OpenAI models. Swap in Claude as a sub-agent and you lose guardrail enforcement entirely—the
tripwire_triggeredmechanism depends on OpenAI’s tool-calling format. - Pre-1.0 API. Core primitives are stable at v0.12.5. Edge-case parameters like
custom_output_extractorandis_enabledare more likely to change. Pin your SDK version.
Where This Pattern Breaks Down
The agents-as-tools pattern solves coordination for bounded tasks. Long-running autonomous workflows—the kind that crash mid-execution and need to resume days later—need durable orchestration on top. The SDK’s Sessions handle conversation history; they don’t handle crash recovery. Temporal or a custom queue layer fills that gap, but that’s infrastructure most teams won’t need until they outgrow single-request workflows.
The agents-as-tools pattern is an architectural stance, not just a coding choice. Centralized control is how you enforce guardrails at scale. The microservices-to-modular-monolith correction took the industry five years. The agent equivalent doesn’t have to—start centralized, decompose later when you have the observability to justify it. Track the SDK’s GitHub repository for the 1.0 milestone that turns these patterns from educated bets into production foundations.
Get the Daily Pulse
Sharp analysis on what's actually moving in AI. No hype, no filler, no weekly digest.



