OpenAI Agents SDK: handoffs, guardrails and tools, explained
What goes into new Agent({ … }) — the difference between a handoff and an agent as a tool, how needsApproval pauses a run, why guardrails are agents themselves, and where MCP servers plug in.
Everything is an object
The OpenAI Agents SDK is OpenAI's own TypeScript framework for agents, published as @openai/agents. Its defining trait is how little ceremony there is: an agent is a constructor call, a tool is a function call, a guardrail is an object literal, and a run is run(agent, input).
import { Agent, run } from "@openai/agents"
const agent = new Agent({
name: "Support agent",
instructions: "You answer questions about the product…",
tools: [lookupOrder],
handoffs: [bookingAgent],
})
const result = await run(agent, "Where is order 10432?")
console.log(result.finalOutput)
Because nothing is discovered by path, the one file that matters is the one where every identifier meets. The free OpenAI Agents SDK generator emits that as src/agent.ts and puts each tool, sub-agent, guardrail and MCP server in its own file so they can be edited separately — a reading convenience, not a registration mechanism.
Two fields are required on every Agent: name and instructions. The name labels traces and becomes the handoff tool name when another agent transfers to this one; the instructions are the system prompt, and nothing is inherited from an agent that delegates here.
Tools: tool() with Zod parameters
import { tool } from "@openai/agents"
import { z } from "zod"
const lookupOrder = tool({
name: "lookup_order",
description: "Look up one order by its id and return its status and delivery estimate.",
parameters: z.object({
orderId: z.string().describe("The order id, as printed on the confirmation email."),
}),
async execute({ orderId }) {
return await orders.get(orderId)
},
})
The name is what the model calls, verbatim, and it lives in one flat list with everything else the agent can call — hosted tools, hosted MCP servers, and other agents wrapped as tools. Two things answering to one name means one of them is unreachable, and nothing says which. The generator checks collisions across all three kinds for that reason.
One Zod detail the SDK's strict schemas impose: an optional field is spelled .nullable(), not .optional(). Strict mode requires every key to be present, so an absent value is null.
A handoff is not an agent as a tool
The SDK gives you two ways to involve another agent, and they behave differently in a way that matters for the conversation.
A handoff transfers the conversation. The other agent takes over and answers the user; the original agent is out of the loop. You list the target in handoffs, and its handoffDescription is the only thing the delegating model reads when deciding whether to transfer:
const bookingAgent = new Agent({
name: "Booking Agent",
instructions: "Help users with booking requests.",
handoffDescription: "Handles anything about a reservation.",
})
const triage = Agent.create({
name: "Triage",
instructions: `${RECOMMENDED_PROMPT_PREFIX}\n…`,
handoffs: [bookingAgent],
})
Two spellings in that snippet are deliberate. Agent.create rather than new Agent keeps finalOutput typed across the handoff targets. And RECOMMENDED_PROMPT_PREFIX, from @openai/agents-core/extensions, is a paragraph the docs suggest prepending to any agent that has handoffs — it tells the model it is in a multi-agent system and how transfers work. The generator prepends it by default whenever a handoff exists and drops the import otherwise.
An agent as a tool reports back. agent.asTool({ toolName, toolDescription }) exposes the other agent as a function. The main agent calls it, gets the result, and keeps the floor:
tools: [
refundExpert.asTool({
toolName: "refund_expert",
toolDescription: "Answers refund policy questions.",
}),
]
Use a handoff for triage that routes to a specialist who should own the rest of the conversation. Use a tool when the main agent should stay in charge and merely consult. The generator makes the choice per agent and, in tool mode, requires a toolName, because asTool() does.
needsApproval pauses the run, and the state is portable
A tool with needsApproval: true does not execute. The run returns early with result.interruptions, one entry per pending call:
let result = await run(agent, question)
while (result.interruptions?.length) {
for (const interruption of result.interruptions) {
console.log(`${interruption.agent.name} wants ${interruption.name}(${interruption.arguments})`)
result.state.approve(interruption) // or result.state.reject(interruption)
}
result = await run(agent, result.state)
}
approve and reject take { alwaysApprove: true } / { alwaysReject: true } for sticky decisions, and a rejection can carry a message the model reads. The part that makes this production-grade rather than a demo: result.state.toString() serialises the paused run, and RunState.fromString(agent, json) restores it — so the review can happen in another process, hours later, with the server long gone. The generated entry point approves everything so the script completes; that loop is the first thing to replace.
Guardrails are agents too
Every guardrail example in the SDK's docs is the same shape: a small checker agent with a structured outputType answers a yes/no question, and the guardrail trips when the answer is yes.
const checker = new Agent({
name: "Math homework check",
instructions: "Is the user asking you to do their math homework? …",
outputType: z.object({ flagged: z.boolean(), reasoning: z.string() }),
})
const mathHomework: InputGuardrail = {
name: "Math homework",
async execute({ input, context }) {
const result = await run(checker, input, { context })
return {
outputInfo: result.finalOutput,
tripwireTriggered: result.finalOutput?.flagged ?? false,
}
},
}
An input guardrail runs before the main agent spends anything and throws InputGuardrailTripwireTriggered. An output guardrail receives agentOutput — the structured object when the main agent has an outputType, otherwise text — and throws OutputGuardrailTripwireTriggered. Both are exceptions, so the entry point catches them; the generator writes that catch and types an output guardrail as OutputGuardrail<typeof OutputType> when structured output exists, which is why the output schema lives in its own module rather than next to the agent that imports the guardrail.
Hosted tools and MCP servers
Four hosted tools run on OpenAI's side through the Responses API and are one line each: webSearchTool(), fileSearchTool(["vs_…"]) over vector stores you have uploaded to, codeInterpreterTool(), and imageGenerationTool(). A file search with no vector store id has nothing to search, so the generator leaves it out and says so.
MCP comes in three kinds. A hosted server is also run by OpenAI — hostedMcpTool({ serverLabel, serverUrl, requireApproval: "always" }) — and sits in the tools array. A streamable HTTP server (MCPServerStreamableHttp({ url })) and a stdio server (MCPServerStdio({ fullCommand })) are connected from your process and go in mcpServers; they are sockets and child processes, so open them before the run and close them in finally, whatever happened in between.
Structured output, turns and tracing
outputType: z.object({ … }) makes result.finalOutput a typed object and makes the model answer in that shape. run(agent, input, { maxTurns }) caps the loop — the default is 10, and exceeding it throws MaxTurnsExceededError rather than billing you forever. Tracing goes to the OpenAI dashboard by default; setTracingDisabled(true) turns it off.
Where to start
npm install @openai/agents zod
Set OPENAI_API_KEY, construct an Agent with instructions and one tool, and run it. If you would rather see the whole project first — the handoff with its prefix, the approval loop, an agent-backed guardrail, the MCP lifecycle — the OpenAI Agents SDK generator produces it as a downloadable .zip, free and without an account.
Its siblings cover the Claude Agent SDK, LangChain Deep Agents and Google ADK, and the MCP server generator builds the servers any of them can mount.
Target-specific scaffolding
Continue this learning path
Permissions, manifests, capabilities, agent projects and MCP servers for browser extensions, desktop, mobile, and backend agents.
Design your application
Design Web, Desktop, Mobile, Terminal, or Extension projects with a target-aware canvas and exporter.
Open projects