OpenAI Agents SDK skill for Codex

Few agents, bounded runs, guardrails that block. Codex designs Agents SDK workflows with the fewest agents that do the job, picks handoffs only when a specialist should own the conversation, and runs input guardrails before side-effecting tools can fire. Every run gets a turn limit, one state strategy and a named trace, so loops and leaks show up in tests instead of production.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- openai-agents-sdk

Installs to ~/.agents/skills/openai-agents-sdk/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $openai-agents-sdk, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds agent workflows with the OpenAI Agents SDK (openai-agents for Python, @openai/agents for TypeScript) — Agent definitions, tools, handoffs versus agents-as-tools, input/output/tool guardrails, sessions, max_turns, tracing and typed outputs. Use when creating a triage or multi-specialist agent, adding guardrails or human approval to an agent, debugging handoff loops or MaxTurnsExceeded, or deciding whether a plain Responses API loop is enough.

Use it when

  • Building a multi-specialist assistant on OpenAI models
  • Adding input, output or tool guardrails to an agent
  • Persisting agent conversations with sessions
  • Debugging handoff loops or MaxTurnsExceeded errors

Use something else when

How to check the result

Scenario tests assert the final agent and tool calls for each input, adversarial inputs trip the guardrail, and a low max_turns run proves MaxTurnsExceeded is caught.

The mistake it prevents

Leaving input guardrails in parallel mode on an agent with side-effecting tools, so the refund runs before the tripwire fires.

~/.agents/skills/openai-agents-sdk/SKILL.md

--- name: openai-agents-sdk description: Builds agent workflows with the OpenAI Agents SDK (openai-agents for Python, @openai/agents for TypeScript) — Agent definitions, tools, handoffs… license: MIT ---

Done well means: the smallest number of agents that covers the job, each with a narrow instruction set and tools; control flow (handoff vs. agent-as-tool) chosen deliberately; guardrails that block before side effects; every run bounded by max_turns; and traces named so a failed run can be found in minutes.

First decide if you need it

SituationUse
One model call, maybe one toolResponses API directly (openai-responses-api)
One agent, several tools, a loopAgents SDK single Agent (saves writing the loop)
Distinct specialists that should own the conversationAgents SDK with handoffs
A coordinator that calls specialists and composes resultsAgents SDK with agent.as_tool(...)
Durable, resumable graph with checkpoints across daysConsider langgraph-agents

Before writing code

  1. Read AGENTS.md; check whether the repo uses Python (openai-agents) or Node (@openai/agents). Install needs network: pip install openai-agents or npm install @openai/agents zod.
  2. Check the installed version. The SDK moves quickly; current Python docs use from agents.decorators import tool, while older releases use function_tool from agents. Match whatever the installed version exports — check with python -c "import agents.decorators as d; print(dir(d))".
  3. Model: set it per agent from config, or globally with RunConfig(model=...). Check the models page; do not hard-code a name copied from an old example.

Core pattern (Python)

import os
from pydantic import BaseModel
from agents import Agent, Runner, RunConfig, RunContextWrapper
from agents.decorators import tool  # older SDKs: from agents import function_tool

MODEL = os.environ["OPENAI_MODEL"]

@tool
async def lookup_order(ctx: RunContextWrapper, order_id: str) -> str:
    """Fetch an order's status.

    Args:
        order_id: Order id such as ORD-12345.
    """
    order = await ctx.context.db.get_order(order_id)  # app deps travel via context
    return order.model_dump_json() if order else "not_found"

class Answer(BaseModel):
    reply: str
    escalate: bool

support = Agent(
    name="Support",
    instructions="Answer order questions. Never promise refunds.",
    tools=[lookup_order],
    output_type=Answer,
    model=MODEL,
)

result = await Runner.run(
    support,
    "Where is ORD-12345?",
    context=app_ctx,                       # your dataclass; never sent to the model
    max_turns=8,
    run_config=RunConfig(workflow_name="support-bot"),
)
answer: Answer = result.final_output

Docstrings become tool descriptions and Args: become parameter descriptions — write them for the model. The context object is local dependency injection (DB handles, user id); the model never sees it.

Handoffs vs. agents as tools

from agents import handoff
from agents.extensions import handoff_filters
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX

billing = Agent(name="Billing", handoff_description="Invoices, charges, payment methods",
                instructions=f"{RECOMMENDED_PROMPT_PREFIX}\nResolve billing questions.")
refunds = Agent(name="Refunds", handoff_description="Refund requests and status",
                instructions=f"{RECOMMENDED_PROMPT_PREFIX}\nProcess refund requests.")

triage = Agent(
    name="Triage",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}\nRoute to the right specialist.",
    handoffs=[billing, handoff(refunds, input_filter=handoff_filters.remove_all_tools)],
)

# Coordinator keeps control and calls a specialist like a function:
translator = Agent(name="Translator", instructions="Translate to Spanish.")
writer = Agent(name="Writer", tools=[translator.as_tool(
    tool_name="translate_to_spanish",
    tool_description="Translate the given text to Spanish")])
  • Handoff: the receiving agent takes over the conversation and produces the final output. Use when the specialist should talk to the user directly. result.last_agent tells you who answered.
  • as_tool: the caller keeps control and receives the specialist’s output as a tool result. Use for composition, fan-out, or when you need one consistent output type.
  • Give every handoff target a handoff_description; triage routes on it.

Guardrails

from agents import GuardrailFunctionOutput, InputGuardrailTripwireTriggered
from agents.decorators import input_guardrail

class Screen(BaseModel):
    off_topic: bool

screener = Agent(name="Screen", instructions="Is this unrelated to our product?",
                 output_type=Screen, model=MODEL)

@input_guardrail(run_in_parallel=False)   # block BEFORE the main agent spends tokens or calls tools
async def on_topic(ctx, agent, input):
    r = await Runner.run(screener, input, context=ctx.context)
    return GuardrailFunctionOutput(output_info=r.final_output,
                                   tripwire_triggered=r.final_output.off_topic)

support.input_guardrails = [on_topic]

try:
    await Runner.run(support, user_text, max_turns=8)
except InputGuardrailTripwireTriggered:
    reply = "I can only help with orders."

Facts that bite:

  • Input guardrails run only for the first agent in the run; output guardrails only for the agent that produces the final output. Specialists reached by handoff are not re-screened on input.
  • Input guardrails default to running in parallel with the agent, so tools may already have run when the tripwire fires. Use run_in_parallel=False when the agent has side-effecting tools. If your installed version lacks the parameter, run the check before Runner.run yourself.
  • Tool guardrails (tool_input_guardrail, tool_output_guardrail) wrap individual function tools and run on every call — use them for argument validation and secret redaction. They do not cover hosted tools or handoffs.

Sessions and state

from agents import SQLiteSession
session = SQLiteSession(f"user-{user_id}", "agents.db")  # omit path for in-memory
await Runner.run(support, "Where is my order?", session=session)
await Runner.run(support, "And when will it arrive?", session=session)

Use exactly one of: session=, conversation_id=, previous_response_id=, or manual result.to_input_list(). They are mutually exclusive. Production options include SQLAlchemy, Redis and encrypted sessions under agents.extensions.memory.

Tracing

  • Tracing is on by default and exports to the OpenAI traces dashboard. Set RunConfig(workflow_name=..., group_id=<thread id>) so runs are findable.
  • Wrap multi-run workflows in with trace("Nightly triage"): to get one trace.
  • PII: set trace_include_sensitive_data=False in RunConfig (or env OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA=false) to drop model/tool payloads.
  • Disable entirely with OPENAI_AGENTS_DISABLE_TRACING=1; tracing is unavailable for Zero Data Retention organizations. Add your own exporter with add_trace_processor().

Gotchas

  • No max_turns → a handoff ping-pong or tool retry loop burns budget until the SDK default limit. Set it explicitly and catch MaxTurnsExceeded.
  • Tool exceptions are by default returned to the model as error text, which can loop. Return structured error strings the model can act on, or pass failure_error_function=None to re-raise.
  • Side-effecting tools (refunds, emails, deletes) need a human approval step; the SDK supports approval on tools (needs_approval) in recent versions — confirm in the installed version, otherwise gate in your own code.
  • Agents that share one long instruction blob route poorly; keep each under a screen.
  • output_type must be JSON-schema friendly (Pydantic model or dataclass); a root union is not allowed in strict structured output.

Verification

  • Unit-test each tool function directly, without the model.
  • Scenario tests: a list of (input, expected_last_agent, expected_tool_calls) run against the real model behind an opt-in flag (needs network and spend).
  • Assert guardrail tripwires on 5–10 adversarial inputs.
  • Open one trace and confirm span names, tool args and no leaked PII.
  • Run with max_turns low (e.g. 3) to prove the exception path is handled.

Output contract

**Agents:** Triage → Billing, Refunds (handoffs); Writer uses Translator as tool
**Tools:** lookup_order (read), issue_refund (write, approval required)
**Guardrails:** on_topic (input, blocking), redact_secrets (tool output)
**State:** SQLiteSession keyed by user id
**Limits:** max_turns=8; tracing workflow_name="support-bot", sensitive data off
**Tests:** <files and results>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "OpenAI Agents SDK"
  short_description: "Few agents, bounded runs, guardrails that block"
  default_prompt: "$openai-agents-sdk Build a triage agent that hands off to billing and refunds specialists"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem