OpenAI Agents SDK Intro
Lesson 07 · OpenAI Agents SDK · ~12 min read
1. What the OpenAI Agents SDK is — and what it isn’t
The first source of confusion is the name. The OpenAI Agents SDK is
not part of the openai Python client, and it is
not the older Assistants API (which was a remote, stateful
service hosted by OpenAI). It is a local Python framework distributed in the
openai-agents PyPI package, and it uses your model provider as a
transport rather than as a runtime. pip install openai-agents pulls
the regular openai client in as a dependency, but every import you
write in this module comes from a top-level agents namespace —
no openai. prefix.
The SDK can drive OpenAI’s own models out of the box and other providers
through a LiteLLM adapter, although as of 2026 Q2 the non-OpenAI paths still
have rough edges (tool-calling parity, streaming semantics, error mapping). To
keep the lessons reproducible we standardise on gpt-4o-mini for
every example.
A brief compass relative to the other three frameworks in this course: the
Claude Agent SDK ships inside the anthropic package; LangGraph
is its own package and assumes you wire in an LLM client yourself; CrewAI is its
own package with stronger opinions about roles and team composition.
Of the four, OpenAI’s shape is closest to the Claude Agent SDK in raw API
ergonomics — but it adds two primitives the Claude SDK does not have
first-class: handoffs and guardrails.
2. The three headline primitives
Almost everything you build with this SDK is some combination of three objects. Slides walk through them in a sentence each; here is a deeper look.
Agent — constructed with at minimum
name=, instructions= (the system prompt), and
model=. Optional but very common: tools=[],
handoffs=[], input_guardrails=[],
output_guardrails=[], and output_type=. The
name field is not cosmetic — when this agent appears as a
handoff target on another agent, its name becomes part of the synthetic tool
name (transfer_to_<name>) and shows up in every trace, so pick
names you’ll be happy to read in logs.
handoff(target_agent, on_handoff=..., input_filter=..., input_type=...)
— the typed wrapper around a handoff target. The shorthand
Agent(..., handoffs=[other]) wraps other in
handoff(other) with defaults. Reach for the explicit
handoff(...) form when you want an on_handoff callback
(logging, audit, metrics), an input_filter (trim or redact the
history before the target sees it), or a Pydantic input_type the
LLM must fill in before the transfer is allowed to happen.
Guardrails — @input_guardrail runs before
the model sees the user input; @output_guardrail runs after
the model produces a final answer. Both return a
GuardrailFunctionOutput whose tripwire_triggered: bool
flag short-circuits the run with a typed exception
(InputGuardrailTripwireTriggered /
OutputGuardrailTripwireTriggered). The output_info
field is freeform and lands in the trace for diagnostics.
3. Runner, sessions, and the final output
The Runner class is the SDK’s loop driver. You hand it the
starting agent and an input, and it runs the inner think–act loop until
the agent produces a final answer. Three entry points:
await Runner.run(agent, input, ...)— async callers.Runner.run_sync(agent, input, ...)— synchronous (most of this lesson uses sync to keep the snippets short).Runner.run_streamed(agent, input, ...)— event streaming for UIs that want token-by-token output.
All three return a RunResult with the following fields:
final_output (the answer), last_agent (which agent
actually produced it, after any handoffs), new_items (the messages
produced during this run), and raw_responses (provider-level
metadata for debugging).
If you set output_type=SomePydanticModel on the agent,
final_output is a typed instance rather than a string —
internally the SDK injects a synthetic “final-answer” tool keyed on
the Pydantic schema, validates the LLM’s arguments against it, and returns
the parsed object. That’s the same final-answer-tool pattern Module 2
introduced by hand, packaged here as a constructor flag.
For multi-turn chats across Runner.run calls, pass a
Session object that holds prior messages. The SDK ships
SQLiteSession out of the box; production deployments often wire a
Redis- or Postgres-backed session instead.
4. Handoffs in depth
The mental model: a handoff is the LLM saying “this isn’t my job — give it to the billing agent.” The SDK realises that intent in three steps:
-
It compiles each
handoffs=[…]entry into a synthetic tool on the current agent, namedtransfer_to_<target_name>, with the target’s instructions as the tool description so the model knows when to call it. -
When the LLM calls a handoff tool, the SDK records the transfer in the trace,
swaps the active agent to the target, and continues the same
conversation loop — the target sees the prior message history (filtered
if
input_filter=is set) and its own instructions / tools / handoffs. -
The loop exits when whichever agent is currently active produces a
final_output.result.last_agenttells you which agent that was, so you can route the answer to the right channel downstream.
handoffs=[triage] on the
billing agent — the SDK supports cycles. The Module 4 lab keeps it acyclic
(triage → specialists) because it’s easier to reason about; the L08
homework asks you to add a one-cycle “escalation” path.
5. Guardrails as a safety belt
The SDK’s most-distinctive design decision is separating cheap pre/post checks from the main loop. Two motivations drive that choice:
-
Cost. A guardrail can refuse a request before it ever reaches
gpt-4o. Block obvious PII, off-topic queries, or oversized inputs at the door for essentially $0. -
Auditability. A guardrail’s
output_infolands in the trace, giving you a structured “we blocked this because X” record sitting right next to the LLM call it would have made.
The input_guardrails=[…] argument accepts a list —
each guardrail runs in parallel before the model call and the first
tripwire short-circuits the run. output_guardrails=[…] are
similar but run after the final output (typed via
output_type if you’ve set one). The SDK doesn’t ship
pre-built guardrails — those are application logic by nature — but
the decorator + tripwire pattern is consistent enough that you can lift one
between projects unmodified.
@input_guardrail calling
Runner.run(small_classifier_agent, …)), but that adds cost
and latency. Reserve heavyweight guardrails for high-stakes verticals; for
everything else a quick regex or string check is the right tool.
6. Tools, sessions, and structured outputs
A brief tour of the supporting surface area, in case you need any of these in the L07 lab or homework:
@function_tool — plays the same role as the
Claude SDK’s @tool. Reads the Python type hints and the
docstring, builds a JSON Schema, and registers the tool on whichever
Agent includes it in tools=[]. Return values can be
strings, Pydantic models, or plain dicts; the SDK serialises them for the model.
Exceptions raised inside the function become is_error tool results
so the LLM can retry rather than crashing the run.
Session — multi-turn state.
SQLiteSession("path.db", session_id="user-42") gives you a
persistent chat history without writing a database layer. Pass
session= to Runner.run(...) and the SDK appends each
turn automatically — you don’t need to manage a
messages list yourself.
output_type= with Pydantic — preferred over
hand-rolling a final-answer tool whenever you control the agent’s output
schema. The SDK validates the model’s output against the schema, retries
on validation errors (the retry count is configurable), and returns a typed
instance via result.final_output. Module 6 will compare this
with Module 2’s manual write_report pattern.
7. When to pick the OpenAI Agents SDK (and when not to)
The decision matrix you will revisit in Module 6, surfaced here so you can spot the sweet spot when L08’s triage-and-handoff demo lands.
Pick the OpenAI Agents SDK when:
- Your agent shape is multiple specialists sharing a conversation — triage front-desks, tiered support, “expert” sub-agents the front agent escalates to.
-
You want structured output by default (
output_type=) rather than parsing strings or writing a final-answer tool every time. - You want pre/post guardrails as first-class plumbing — content filters, PII gates, output validators that the framework runs around your loop without you wiring them in by hand.
- You are already inside the OpenAI ecosystem (model contracts, billing, tracing in OpenAI’s UI).
Don’t pick the OpenAI Agents SDK when:
- It’s one agent + many tools — the Claude SDK is leaner for that shape (Module 2 covered it).
- You want explicit DAG control flow with checkpointing and pause/resume — LangGraph (Module 3) is the right pick.
- You want a team of agents collaborating in parallel with role-based assignment — CrewAI (Module 5) is the right pick.
- Your model provider is not OpenAI and you’re not willing to ride the LiteLLM adapter’s rough edges as of 2026 Q2.
This is the framing L08 builds on: the triage-and-handoff customer-support
agent is the canonical sweet-spot demo, and every primitive you just read about
— Agent, handoff(), guardrails,
output_type= — shows up in the next lesson’s code.
References
-
OpenAI Agents SDK docs —
openai.github.io/openai-agents-python/(Agents, Handoffs, and Guardrails pages). - Building Effective Agents — Anthropic engineering, 2024 (the conceptual ancestor of “handoff” patterns).
-
L03 —
agents/lesson03-claude-agent-sdk-intro/theory.html, Claude Agent SDK primer, for contrast on shape and ergonomics. -
L05 —
agents/lesson05-langgraph-intro/theory.html, LangGraph primer, for contrast on explicit DAG control flow.