On this page
- Why the Agents SDK changes the ownership boundary
- The production architecture I would start with
- A minimal runnable workflow with handoffs, guardrails, and tracing
- State, streaming, and interruption semantics
- Production tradeoffs and common mistakes
- Operational checklist
- FAQ
- When should I use Agents SDK instead of the Responses API?
- Should I use handoffs or agents as tools?
- Are SDK sessions enough for production memory?
- Does tracing store sensitive data?
- References
OpenAI Agents SDK is worth using when you want the runtime to own the loop: tool calls, handoffs, guardrails, sessions, approvals, and traces. I would not use it to hide architecture. I would use it where a service has clear workflow boundaries, observable side effects, and explicit recovery paths.
Why the Agents SDK changes the ownership boundary
The interesting part of the OpenAI Agents SDK is not that it gives you an Agent class. Most teams already have some version of that in a wrapper around prompts, tools, and a model client. The important shift is ownership. With the SDK, the loop that decides whether to call a tool, hand off to another specialist, run another model turn, pause for approval, or return a final answer moves from your application code into the runner.
That can be a good trade. The official API guide draws the boundary plainly: use the Responses API when you want direct control over model interactions, output items, tool execution, state, and branching. Use the Agents SDK when you want the SDK to manage recurring orchestration such as repeated tool calls, handoffs, sessions, tracing, guardrails, or resumable approvals.
The primitives are deliberately small. The overview names agents, agents as tools or handoffs, guardrails, and tracing as the core concepts. The agent definition guide adds the operational point: Agent plus Runner lets the SDK manage turns, tools, guardrails, handoffs, and sessions. That is a meaningful contract, but it also means your application must be opinionated about what the runner is allowed to do.
Engineering judgment: do not adopt an agent framework because it makes a demo shorter. Adopt it when it removes duplicated loop code and gives you better hooks for state, review, and observability than your local abstraction.
The production architecture I would start with
For a production service, I would not put the SDK directly behind a public chat endpoint and call it done. I would wrap it in a workflow service with explicit ingress, state, tool policy, approval, and telemetry boundaries.
graph TD
A["API request"] --> B["Workflow service"]
B --> C["Policy and input checks"]
C --> D["Agents SDK Runner"]
D --> E["Specialist agents"]
D --> F["Function tools"]
D --> G["Hosted tools"]
F --> H["Internal systems"]
D --> I["Approval queue"]
I --> D
D --> J["Session store"]
D --> K["Trace export"]
D --> L["Final response"]
The running agents guide describes the runner loop as a sequence of model calls, tool calls, handoffs, approval interruptions, and final outputs. That is exactly why I keep the runner inside a workflow service. The API tier should authenticate, rate-limit, and normalize the request. The workflow tier should decide which starting agent is allowed, which tools are attached, which session strategy applies, and what to do when the run pauses.
The tools guide matters here because tools are not one thing. Hosted OpenAI tools, local runtime tools, function tools, agents as tools, and experimental Codex tools have different blast radii. A function tool that reads from an internal billing system needs different approval and logging rules than a hosted web search tool. A local shell-style capability belongs in a much tighter sandbox than a pure classifier function.
Handoffs are another boundary worth treating carefully. The handoffs guide says handoffs are represented to the model as tools, such as transfer_to_refund_agent. That detail is useful. I would log handoffs like any other routing decision because they change which instructions, tools, and output type own the rest of the turn.
A simple service decomposition looks like this:
| Boundary | What it owns | Failure mode to design for |
|---|---|---|
| API tier | Auth, tenant, quota, request shape | Invalid user input reaches the workflow |
| Workflow service | Starting agent, run config, session strategy | Wrong agent or tool set selected |
| Tool layer | Side effects and external calls | Partial writes, slow dependencies, unsafe arguments |
| Approval queue | Human review and resume state | User waits forever or stale approval resumes |
| Observability | Traces, metrics, audit logs | Debugging depends on raw transcripts only |
| Session store | Conversation continuity | Context grows without pruning or mixes tenants |
A minimal runnable workflow with handoffs, guardrails, and tracing
The current openai-agents package is shown as v0.18.2 on GitHub, and the repository quickstart requires Python 3.10 or newer. The example below is intentionally small. It shows the shape I care about: one triage agent, one specialist, one guarded function tool, an input guardrail, and an explicit trace.
import asyncio
from dataclasses import dataclass
from agents import (
Agent,
GuardrailFunctionOutput,
InputGuardrailTripwireTriggered,
RunContextWrapper,
Runner,
function_tool,
input_guardrail,
trace,
)
@dataclass
class AccountContext:
tenant_id: str
user_id: str
can_read_billing: bool
@function_tool
def get_invoice_status(invoice_id: str) -> str:
"""Return invoice status for a known invoice id."""
if not invoice_id.startswith("inv_"):
return "Invalid invoice id format."
return "paid"
@input_guardrail(run_in_parallel=False)
async def billing_scope_guardrail(
ctx: RunContextWrapper[AccountContext],
agent: Agent,
input: str,
) -> GuardrailFunctionOutput:
allowed = ctx.context.can_read_billing and "password" not in input.lower()
return GuardrailFunctionOutput(
output_info={"allowed": allowed, "user_id": ctx.context.user_id},
tripwire_triggered=not allowed,
)
billing_agent = Agent(
name="Billing specialist",
instructions=(
"Answer billing questions. Use get_invoice_status only when the "
"user provides an invoice id. Do not invent account data."
),
tools=[get_invoice_status],
)
triage_agent = Agent(
name="Support triage",
instructions="Route billing questions to the billing specialist.",
handoffs=[billing_agent],
input_guardrails=[billing_scope_guardrail],
)
async def main() -> None:
context = AccountContext(
tenant_id="tenant_123",
user_id="user_456",
can_read_billing=True,
)
try:
with trace("support_billing_workflow"):
result = await Runner.run(
triage_agent,
"Is invoice inv_001 paid?",
context=context,
)
print(result.final_output)
print(f"last_agent={result.last_agent.name}")
except InputGuardrailTripwireTriggered:
print("Request blocked before the agent ran.")
if __name__ == "__main__":
asyncio.run(main())
The guardrail is blocking on purpose. The guardrails guide says parallel input guardrails are the default, but a parallel guardrail may allow token use or tool execution before cancellation. For authorization-like checks, I prefer run_in_parallel=False even if it adds latency, because the point is to prevent the agent from starting.
State, streaming, and interruption semantics
State is where agent demos usually age badly. The SDK gives you several options, but it does not remove the need to choose one deliberately.
The sessions guide says a session retrieves conversation history before each run and stores new items after each run. It also says sessions cannot be combined with conversation_id, previous_response_id, or auto_previous_response_id in the same run. That is a small sentence with large consequences. Pick one continuation strategy per workflow. Do not layer three memory mechanisms together because each one seemed useful in isolation.
The same sessions guide lists built-in storage choices including SQLite, Redis, SQLAlchemy, MongoDB, Dapr, OpenAI Conversations, compaction wrappers, advanced SQLite, and encrypted sessions. I would treat SQLite as a local development default, Redis as a reasonable low-latency shared store, and SQLAlchemy or an existing application database as the boring production choice when governance matters. The right answer depends less on the SDK and more on your retention and tenancy model.
Results deserve the same attention. The results guide says RunResultBase exposes final_output, new_items, last_agent, raw_responses, and to_state(). I would persist new_items or a normalized audit projection when a workflow has side effects. I would not rely only on the final answer, because the final answer does not explain which tool ran or which specialist owned the turn.
Approval flows are another reason to avoid hiding the runner behind a thin controller. The results guide says runs that pause for approval expose interruptions and to_state() so the application can approve or reject and resume. That state should live in your workflow database with an expiration time, a reviewer identity, and a replay policy.
Streaming has a subtle completion rule. The streaming guide says a streaming run is not complete until result.stream_events() finishes, and post-processing such as session persistence, approval bookkeeping, or compaction can finish after the last visible token. If your UI stops reading after the last text delta, your backend may think the run is done before the SDK has finalized the state you care about.
Production tradeoffs and common mistakes
The first tradeoff is control. The official API guide says the Responses API is the better fit when you want to own custom loops and branching directly. That is not a footnote. If your application has a compliance-reviewed state machine, deterministic step transitions, or a scheduler that already owns retries, a hand-rolled Responses loop can be easier to reason about than a general agent runner.
The second tradeoff is observability. The tracing guide says tracing is enabled by default, can be disabled globally, in code, or per run, and is unavailable for organizations operating under Zero Data Retention. It also says generation and function spans may capture sensitive inputs and outputs unless you change RunConfig.trace_include_sensitive_data or OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA. I would make that setting part of deployment configuration, not a developer preference hidden in a sample script.
The third tradeoff is provider abstraction. The models guide describes third-party adapters as beta and advises teams to validate structured outputs, tool calling, usage reporting, and provider-specific behavior for the exact backend they plan to deploy. That is a practical warning. Multi-provider demos are easy. Multi-provider incident response is not.
Here is the comparison I use when reviewing an architecture proposal:
| Choice | Use it when | Be careful about |
|---|---|---|
| Responses API loop | You need exact control over branching, persistence, and retries | You own every tool loop and orchestration edge case |
| Agents SDK with handoffs | Specialists should take over a turn with their own instructions | Routing decisions need logging and tests |
| Agents as tools | A manager should stay in control while specialists do bounded work | Tool outputs can hide specialist reasoning if you log too little |
| SDK sessions | You want quick managed conversation continuity | Retention, pruning, tenant isolation, and deletion policy remain yours |
| SDK tracing | You need built-in visibility across model, tool, guardrail, and handoff spans | Sensitive data defaults and ZDR constraints must be handled explicitly |
A final mistake is putting guardrails only at the edges. The guardrails guide says input guardrails run for the first agent, output guardrails run for the final agent, and tool guardrails wrap function-tool invocations. In workflows with handoffs or managers, that distinction matters. A policy that should protect every database write belongs near the tool, not only on the first user message.
Operational checklist
Before I would ship an Agents SDK workflow, I would want these checks in place:
- The starting agent is selected by application code, not by untrusted user text alone.
- Every tool has a clear owner, timeout, retry policy, idempotency rule, and audit event.
- Tool input guardrails protect side-effecting tools, especially when handoffs or agents as tools can reach them.
- Blocking guardrails are used for authorization, tenancy, and side-effect prevention.
- The continuation strategy is singular: SDK session, server-managed conversation, previous response chain, or manual history, not all of them.
- Approval interruptions are persisted with reviewer identity, expiration, and resume checks.
- Streaming consumers drain
stream_events()to completion before marking the run complete. - Trace settings are reviewed for sensitive data capture, ZDR constraints, and export latency.
- Handoffs are logged as routing decisions with the previous agent, next agent, and reason when available.
- Evals cover tool selection, handoff routing, refusal paths, approval pauses, and session pruning.
The tracing guide notes that long-running workers can call flush_traces() after a trace context exits when they need immediate export. I would use that in queue workers where a job result and its trace need to appear together during incident review.
The guardrails guide also notes that tool input guardrails normally run after approval and immediately before execution, with a pre-approval option available through RunConfig.tool_execution. That is exactly the kind of operational detail that should become a test. If approval screens show tool arguments, validate dangerous arguments before a human sees an approval prompt, then validate again before execution.
FAQ
When should I use Agents SDK instead of the Responses API?
Use the Agents SDK when you want the SDK to own repeated tool loops, handoffs, sessions, tracing, guardrails, or approval resume flows. Use the Responses API when your application should own the loop directly.
Should I use handoffs or agents as tools?
The orchestration guide says agents as tools fit manager-style workflows where one agent stays in control, while handoffs fit delegated ownership where a specialist takes over. I prefer agents as tools for bounded subtasks and handoffs when the user-facing owner should actually change.
Are SDK sessions enough for production memory?
The sessions guide gives useful built-ins, including Redis and SQLAlchemy-backed options, but production memory still needs tenancy, retention, deletion, pruning, and audit decisions. The SDK can store conversation items. It cannot decide your product's data policy.
Does tracing store sensitive data?
It can. The tracing guide says generation and function spans may capture sensitive data by default, and that teams can control this through RunConfig.trace_include_sensitive_data or OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA. Review that before shipping, especially if your organization has strict data retention requirements.