OpenAI Agents SDK¶
You have an agent built with the OpenAI Agents SDK and you want it to run as an Airflow
task, reading data through connections your deployment already manages. Keep the agent
as it is, and add Airflow’s toolsets to its tools with
AirflowTools.
Note
AirflowTools is experimental: it may change in a minor release of this provider.
The framework-neutral tool interface it builds on is stable.
See Stable and experimental features.
Run an OpenAI Agents SDK agent in a task¶
The agent below answers a question about a database with a
SQLToolset. Its model’s API key, and
optionally the base URL of another OpenAI-compatible endpoint, come from an Airflow
connection:
@dag(tags=["example"])
def example_openai_agents_agent():
"""Answer a question about a database with an OpenAI Agents SDK agent."""
@task
def run_openai_agent(question: str = DEFAULT_QUESTION) -> str:
from agents import Agent, OpenAIChatCompletionsModel, Runner, set_trace_processors
from openai import AsyncOpenAI
from airflow.providers.common.ai.tools.openai_agents import AirflowTools, AirflowTraceProcessor
from airflow.providers.common.ai.tools.tracing import agent_framework_tracing
from airflow.providers.common.ai.toolsets.sql import SQLToolset
# Record the SDK's traces as the task's OpenTelemetry spans instead of sending them to OpenAI.
set_trace_processors([AirflowTraceProcessor()])
llm = BaseHook.get_connection(LLM_CONN_ID)
agent = Agent(
name="analyst",
model=OpenAIChatCompletionsModel(
model=LLM_MODEL,
openai_client=AsyncOpenAI(api_key=llm.password, base_url=llm.host or None),
),
instructions=(
"You are a SQL analyst. Use list_tables and get_schema to explore "
"the database, then run read-only queries to answer the question."
),
tools=AirflowTools(SQLToolset(db_conn_id=DB_CONN_ID)),
)
with agent_framework_tracing():
return Runner.run_sync(agent, question).final_output
run_openai_agent()
Everything except AirflowTools is plain OpenAI Agents SDK: the agent, its runner,
handoffs, guardrails and your own function tools work as the SDK documentation
describes. AirflowTools is a list of FunctionTool objects, so add your own tools
next to them with tools=[*AirflowTools(warehouse), my_tool].
What the tools give the agent¶
AirflowTools takes the same arguments as the Strands plugin (see Strands Agents):
toolsets that implement ToolProvider, such
as SQLToolset and HookToolset, and individual
AirflowTool objects. MCPToolset is the
exception: attach an MCP server with the SDK’s own mcp_servers=. Each tool keeps the
toolset tool’s name, description and argument schema, runs through the toolset, and
passes its result through Airflow’s secret masker.
The schema goes to the model as it is, with
strict_json_schema=False. The SDK’s strict mode would mark every argument required, including the ones a toolset leaves optional.A failure the model can correct, such as a rejected SQL statement or an invalid argument, reaches it as
{"error": ...}. The tool’s retry limit bounds how many times in a row the model can try again, counted afresh on every run of the agent.A refusal the tool reports as final, such as a file that does not exist, also comes back as
{"error": ...}without counting against that limit, and so do arguments that are not a JSON object. Themax_turnsargument ofRunner.runbounds how long a model can keep asking.Any other failure raises
ToolCallErrorout ofRunner.runandRunner.run_sync, with the message masked. The task fails and Airflow’s own retry takes over.
Outside AgentOperator, a toolset’s connection ID is used as written: it is not
rendered as a template.
An agent used as a tool¶
Agent.as_tool() turns any exception from the inner agent’s run into text for the
outer model by default, so a tool failure that should fail the task would only reach the
orchestrating model as a message. When the inner agent holds AirflowTools, pass
failure_error_function=None:
analyst = Agent(name="analyst", model=model, tools=AirflowTools(warehouse))
lead = Agent(
name="lead",
model=model,
tools=[analyst.as_tool("ask_analyst", "Ask the SQL analyst.", failure_error_function=None)],
)
The ToolCallError then fails the outer run, and with it the task. The same applies to
a function tool of your own that runs another agent: let the exception propagate. A
nested run shares the outer run’s retry count for each tool.
Tracing¶
The SDK has a tracing system of its own rather than OpenTelemetry. By default it sends
every run’s traces to OpenAI’s Traces backend using OPENAI_API_KEY from the
environment, whatever model the agent runs, including a self-hosted one, and the traces
hold the prompts and model output. Tool results are masked before the model sees them;
nothing else in those traces is.
AirflowTraceProcessor is a trace
processor for the SDK that records each trace as OpenTelemetry spans under the task’s
span instead. The example replaces the SDK’s processors with it, so nothing goes to
OpenAI, and runs the agent inside
agent_framework_tracing(), so the spans
carry the task’s identity:
from agents import Runner, set_trace_processors
from airflow.providers.common.ai.tools.openai_agents import AirflowTraceProcessor
from airflow.providers.common.ai.tools.tracing import agent_framework_tracing
set_trace_processors([AirflowTraceProcessor()])
with agent_framework_tracing():
answer = Runner.run_sync(agent, question).final_output
set_trace_processors replaces every processor registered with the SDK. To keep the
export to OpenAI, or a processor another instrumentation registered, add this one with
agents.add_trace_processor instead. The spans use the names of the OpenTelemetry GenAI
conventions (invoke_agent, chat with the model and token counts,
execute_tool), and the SDK’s other spans are named openai_agents.<kind>. Prompts, completions, and tool arguments and results are left
out unless [common.ai] otel_export_enabled and capture_content are both on, and pass
through Airflow’s secret masker when they are recorded; see Observability (OpenTelemetry tracing).
Use AirflowTraceProcessor when span content should follow [common.ai] capture_content
and Airflow’s secret masker.
To turn the SDK’s tracing off altogether, call agents.set_tracing_disabled(True) or set
OPENAI_AGENTS_DISABLE_TRACING=1 in the worker’s environment. That stops
AirflowTraceProcessor from receiving anything too.
Durable execution¶
When a task that calls Runner.run fails and Airflow retries it, the agent starts
again from the prompt: every model call is paid for again, and every tool call happens
again. On a task with retries, run the agent with
run_durably_sync(), or
run_durably() from async code,
instead. They take the same arguments as Runner.run_sync and Runner.run, except
previous_response_id, conversation_id and auto_previous_response_id, which they
refuse: with them OpenAI keeps the conversation, and a replayed call never reaches it.
@dag(tags=["example"])
def example_openai_agents_durable():
"""Answer a question about a database with an OpenAI Agents SDK agent that survives task retries."""
@task(retries=2, retry_delay=timedelta(minutes=1))
def run_durable_openai_agent(question: str = DEFAULT_QUESTION) -> str:
from agents import Agent, OpenAIChatCompletionsModel, RunConfig
from openai import AsyncOpenAI
from airflow.providers.common.ai.durable.openai_agents import run_durably_sync
from airflow.providers.common.ai.tools.openai_agents import AirflowTools
from airflow.providers.common.ai.toolsets.sql import SQLToolset
llm = BaseHook.get_connection(LLM_CONN_ID)
agent = Agent(
name="analyst",
model=OpenAIChatCompletionsModel(
model=LLM_MODEL,
openai_client=AsyncOpenAI(api_key=llm.password, base_url=llm.host or None),
),
instructions=(
"You are a SQL analyst. Use list_tables and get_schema to explore "
"the database, then run read-only queries to answer the question."
),
tools=AirflowTools(SQLToolset(db_conn_id=DB_CONN_ID)),
)
# The SDK sends traces to OpenAI by default; see the guide's tracing section.
result = run_durably_sync(agent, question, run_config=RunConfig(tracing_disabled=True))
return result.final_output
run_durable_openai_agent()
Each model call and function tool call is recorded in the task instance’s durable
journal as it completes, the journal AgentOperator(durable=True) uses (see
Durable execution). On Airflow 3.3 and later the journal is kept in the task
state store; on earlier versions, set [common.ai] durable_cache_path to where it is
kept. Once the run succeeds, what was recorded is deleted, so clearing the task later
starts over. Outside a task there is nothing to replay into, and the agent runs as
Runner.run runs it.
What a retry replays¶
On a retry the agent runs again from the start, and each call that asks what the failed
try asked, in the same order, returns what it returned then: the calls of every model
given by name or in run_config, the model and function tools of the starting agent
and of the agents in its handoffs list, and an agent used through as_tool, as one
call. Recorded tool results pass through Airflow’s secret masker before they are saved,
and a replayed model call reports no token usage.
From the first call that differs, that call and every call after it run live:
A retry whose prompt, instructions, model settings or tools differ from the failed try’s runs everything live from the first call that sees the difference.
When the run failed because of the model’s answers (
MaxTurnsExceeded,ModelBehaviorError,ModelRefusalError, or an output or tool input guardrail), replaying those answers would fail it the same way, so the retry asks the model again from its last request.
What always runs live¶
A call that was running when the try failed, so tools that change data must still be safe to repeat.
A tool call that failed, including one whose exception
failure_error_functionturned into text for the model. AModelBehaviorErrorthat a tool raises, such as arguments that fail validation withfailure_error_function=None, counts as the tool’s failure: the tool call runs again, but the answer that asked for it replays.Tools other than function tools: hosted tools such as web search, the tools of
mcp_servers, and shell and computer tools.A function tool whose
needs_approvalis a function, since the SDK checks the arguments it approved only through its own invoker.The function tools of an agent reached through
handoff(), and its model too unless the agent’smodelis a model name or not set.A streamed run.
Every call, when the run has a
session: the SDK adds the failed try’s prompt to it before the first model call, so that call no longer asks what it asked then.
A SandboxToolset without attach_to is destroyed when the try ends, so the retry
starts with a new, empty sandbox that the replayed conversation does not describe.
Differences from AgentOperator¶
A task retry runs the agent again from the start unless it runs through
run_durably; see Durable execution.There is no human review step like
AgentOperator(enable_hitl_review=True), and the SDK’s ownneeds_approvalinterruptions are not connected to Airflow’s approval UI.
Installation¶
pip install "apache-airflow-providers-common-ai[sql]" "openai-agents>=0.20.0"
The SDK requires openai>=3 from 0.21.0 on. Airflow’s constraints file pins openai
below 3, so with that file the command above installs openai-agents 0.20, the last
release that accepts openai 2.x. AirflowTools works with both.