OpenAI Agents SDK integration
The Temporal integration for the OpenAI Agents SDK runs its agents as durable Temporal Workflows in TypeScript. The integration keeps the agent loop, tool selection, and handoffs in the Workflow and runs each model call, MCP operation, and sandbox operation as an Activity, because a Workflow Definition must be deterministic and can't call a model, an MCP server, or a sandbox itself.
Activities retry on failure. Completed ones are recorded in Event History and aren't repeated when the Workflow replays, so an agent survives Worker restarts and can run for as long as it needs.
Code snippets on this page come from the OpenAI Agents samples directory.
Install the integration
Before you start:
- This guide assumes you're familiar with the OpenAI Agents SDK. If you aren't, start with the OpenAI Agents SDK documentation.
- If you're new to Temporal, read Understanding Temporal or take the Temporal 101 course.
- Set up your local environment by following Set up a local development environment with the TypeScript SDK, and leave the Temporal development server running to try your code locally.
Install the @temporalio/openai-agents package along with
the OpenAI Agents SDK packages it uses as peer dependencies,
@openai/agents-core,
@openai/agents-openai, and
openai:
npm install @temporalio/openai-agents @openai/agents-core @openai/agents-openai openai
The integration supports a single minor version of the OpenAI Agents SDK packages, so install them in the same command
and npm picks versions that match. Keep every @temporalio/* package in your application on the same version, since the
integration depends on exact versions of them.
Import Worker and Client APIs from @temporalio/openai-agents, and Workflow APIs from
@temporalio/openai-agents/workflow, which is safe to load in the Workflow sandbox. Configuration
lists every entry point.
Run an agent in a Workflow
A Temporal-backed agent has three parts: a Workflow that runs the agent, a Worker with the integration's plugin, and a Client that starts the Workflow.
In the Workflow, run the agent with TemporalOpenAIRunner instead of the OpenAI Agents SDK Runner:
openai-agents/src/basic/workflows.ts
export async function helloWorld(prompt: string): Promise<string> {
const agent = new Agent({ name: 'HelloAgent', instructions: 'You are a helpful assistant.' });
const result = await new TemporalOpenAIRunner().run(agent, prompt);
return result.finalOutput ?? '';
}
Register OpenAIAgentsPlugin on the Worker with a model provider:
openai-agents/src/basic/worker.ts
const worker = await Worker.create({
connection,
taskQueue: 'openai-agents-basic',
workflowsPath: require.resolve('./workflows'),
activities,
plugins: [
new OpenAIAgentsPlugin({
modelProvider: new OpenAIProvider({ apiKey }),
modelParams: { useLocalActivity: true },
}),
],
bundlerOptions: {
webpackConfigHook: (config) => ({
...config,
resolve: {
...config.resolve,
conditionNames: ['require', 'browser', 'default'],
},
}),
},
});
await worker.run();
The plugin registers the Activity that calls the model, along with the interceptors and Workflow bundle polyfills the OpenAI Agents SDK needs. The model provider runs inside that Activity, so only Workers need your model provider's credentials.
Register the plugin on the Client too:
openai-agents/src/basic/client.ts
const connection = await Connection.connect();
const client = new Client({
connection,
plugins: [new OpenAIAgentsPlugin({ modelProvider: new OpenAIProvider({ apiKey }) })],
});
const taskQueue = 'openai-agents-basic';
const workflowId = 'openai-agents-' + nanoid();
The Client's plugin requires a modelProvider but never calls it, so that provider doesn't need credentials. The
plugin sends its modelParams and interceptorOptions to each Workflow the Client starts. modelParams holds the
model Activity options, such as timeouts, a retry policy, or useLocalActivity: true, and interceptorOptions holds
the tracing options.
A Worker plugin's modelParams doesn't reach model calls, even on the Worker that runs them. Set model Activity options
on the Client's plugin or in the runner's defaultModelParams.
Agents name their model with a string, or leave it out to use the provider's default, and the Worker's model provider
resolves it inside the Activity. A model object can't cross into an Activity, so an agent that has one fails the run. To
run every agent in one run on the same model, set runConfig.model to that model's name. Structured output through an
agent's outputType works as in the OpenAI Agents SDK, and the
basic sample shows it along with
run context and a per-run model override.
Any OpenAI Agents SDK ModelProvider works on the Worker. The
model providers sample
points OpenAIProvider at an OpenAI-compatible endpoint by setting its baseURL.
To get a reasoning model's summary, set modelSettings.reasoning.summary on the agent and read the reasoning_item
entries in result.newItems. The
reasoning content sample
takes a different route and calls the openai package directly from its own Activity, outside the agent loop.
The rest of the agent loop runs in the Workflow, including handoffs, guardrails, and instructions functions, so that
code must be deterministic. The context you pass to run() reaches that code and tool() functions, but not
activityAsTool Activities or agentAsTool nested runs.
Each model call is its own Activity, so agents you run together with Promise.all make their model calls in parallel.
The handoffs,
agent patterns, and
multi-agent samples show
triage handoffs, chained and parallel agents, an LLM judge, and guardrails.
In a run without stream: true, an error the OpenAI Agents SDK throws during the run, such as a tripped guardrail or
MaxTurnsExceededError, reaches the Workflow as a non-retryable ApplicationFailure of type AgentsWorkflowError, with
the SDK's error as its cause. Check cause when you catch it, as the agent patterns sample does for guardrails.
Add tools and MCP servers
An OpenAI Agents SDK tool() runs its execute function in the Workflow, so a tool that calls an API, reads a file,
queries a database, or does other I/O has to run in an Activity or a Nexus Operation instead.
Wrap an Activity as a tool with activityAsTool:
openai-agents/src/basic/workflows.ts
export async function tools(prompt: string): Promise<string> {
const weatherTool = activityAsTool<typeof activities.getWeather>(
{
name: 'getWeather',
description: 'Get the current weather for a city.',
parameters: {
type: 'object',
properties: { city: { type: 'string', description: 'The city name' } },
required: ['city'],
additionalProperties: false,
},
},
{ startToCloseTimeout: '1 minute' },
);
const agent = new Agent({
name: 'WeatherAgent',
instructions: 'You are a helpful weather assistant. Always use the getWeather tool to answer weather questions.',
tools: [weatherTool],
});
const result = await new TemporalOpenAIRunner().run(agent, prompt);
return result.finalOutput ?? '';
}
The tool's name must match an Activity registered on the Worker. The model's arguments become the Activity's single
argument, and the Activity's result goes back to the model as a string, or as JSON if it isn't one. activityAsTool's
second argument sets the Activity's timeouts, retry policy, and Task Queue.
Use tool() for deterministic work. It can also call an Activity, as the basic sample's local Activity tool does
through proxyLocalActivities.
nexusOperationAsTool exposes a Nexus Operation as a tool. Pass it the Operation from your service
definition, a tool definition shaped like activityAsTool's, and options with the service and endpoint. The
Workflow runs the Operation and returns its result to the agent, as in the
Nexus tools sample.
To call another agent as a tool, use the integration's agentAsTool rather than the SDK's agent.asTool(), so the
nested agent's model calls run as Activities too. The nested run gets only the parent run's runConfig.sandbox, so the
parent's other run settings, such as traceIncludeSensitiveData and its runConfig guardrails, don't apply inside it,
and agentAsTool has no option to set them. The agent patterns sample uses agentAsTool for an orchestrator that asks
a specialist.
MCP servers run on the Worker, and Workflow code refers to them by name. Register a provider for each server in the
plugin's mcpServerProviders, then add a handle with the same name to the agent's mcpServers:
- Stateless servers:
StatelessMCPServerProvidertakes a name and a factory that returns an OpenAI Agents SDK MCP server, such asMCPServerStdioorMCPServerStreamableHttp. In the Workflow, usestatelessMcpServer(name). Each tool listing and tool call is an Activity that connects to the server, does that one operation, and disconnects, so no state carries between calls. - Stateful servers:
StatefulMCPServerProvideralso takes aNativeConnection, usually the Worker's. In the Workflow, callconnect()on thestatefulMcpServer(name)handle before the run andcleanup()in afinallyblock.connect()starts a long-running Activity that opens the server and runs a dedicated Worker for that Workflow run, so the server keeps its state untilcleanup().
The handles expose a server's tools, not its prompts, so fetch an MCP prompt in your own Activity. The MCP sample does this and shows stdio, Streamable HTTP, SSE, and stateful servers.
Hosted tools from @openai/agents-openai, such as webSearchTool(), imageGenerationTool(), and
codeInterpreterTool(), run inside the model provider during the model Activity, as the
hosted tools sample shows. A
hostedMcpTool() works the same way, so the model provider calls the remote MCP server itself. Its onApproval
callback runs in the Workflow and can wait for a Signal, as in the
hosted MCP sample.
Tool declarations are part of the model Activity's input, so a credential written into one, such as a hosted MCP tool's
authorization, is recorded in Event History. Supply it from the Worker with the plugin's hostedToolCredentials
instead, which keeps it out of that input, as described in Configuration.
Keep conversation history and pause for approval
Agent state lives in the Workflow, so a conversation or a pending approval survives Worker restarts for as long as the Workflow runs.
To keep conversation history across run() calls, pass a WorkflowSafeMemorySession as the session option, since
the runner rejects the SDK's MemorySession. The history is held in the Workflow and rebuilt by replay, but a
Continue-As-New starts with an empty session. To carry it over, pass await session.getItems() to continueAsNew()
and construct the new run's session with initialItems. The
sessions sample shows a
multi-turn chat and the carry-over.
Instead of a session, a long-running Workflow can keep the conversation itself and pass the accumulated
result.history to each run() call, as the
stateful conversation sample
does.
A tool with needsApproval pauses the run, and run() returns with result.interruptions set. Wait for a Signal or
Update, call result.state.approve(interruption) or result.state.reject(interruption) for each entry in
result.interruptions, then resume by passing result.state back to run().
To resume in a fresh Event History instead, serialize the state with result.state.toString() and pass it to
continueAsNew() once the approval arrives. In the new run, rehydrate it with RunState.fromString(), passing an
agent with the same tools, handoffs, and MCP servers as the run that paused, then approve or reject each entry in its
getInterruptions() and pass the state to run(). The
human approval sample
takes this route and includes the Client that sends the approval Signal. For how the SDK serializes RunState, see
Dealing with longer approval times
in OpenAI's human-in-the-loop guide.
Run sandbox agents
A SandboxAgent works in a sandbox, a workspace where it runs shell commands and reads and edits files. The
integration runs each sandbox operation as an Activity, including creating the session, every command and file
operation, and the teardown. Sandbox support is experimental. For manifests, capabilities, and backends, see OpenAI's
sandbox agents concepts guide.
On the Worker, wrap a sandbox backend's client in a SandboxClientProvider under a name, and pass it to the plugin's
sandboxClientProviders:
openai-agents/src/sandboxes/worker.ts
plugins: [
new OpenAIAgentsPlugin({
modelProvider: new OpenAIProvider({ apiKey }),
sandboxClientProviders: [new SandboxClientProvider('local', new UnixLocalSandboxClient())],
resolvableWorkerEnvVars: ['SANDBOX_SERVICE_TOKEN'],
}),
],
UnixLocalSandboxClient, from @openai/agents-core/sandbox/local, runs the agent's commands directly on the Worker
host as the Worker's OS user, with no container or chroot around them. Use it only for local development. To isolate
the agent, register DockerSandboxClient, from the same entry point, or a hosted backend from OpenAI's
sandbox clients guide under the same name.
Only the Worker changes.
In the Workflow, set runConfig.sandbox.client to temporalSandboxClient() with the provider's name:
openai-agents/src/sandboxes/workflows.ts
import { Manifest, SandboxAgent } from '@openai/agents-core/sandbox';
import { TemporalOpenAIRunner, temporalSandboxClient, workerEnvValue } from '@temporalio/openai-agents/workflow';
import { ApplicationFailure } from '@temporalio/workflow';
export async function sandboxWorkflow(prompt: string): Promise<string> {
const agent = new SandboxAgent({
name: 'SandboxAgent',
instructions:
'Use the sandbox to do the work rather than answering from memory, then report what the commands returned. ' +
'Run each command to completion in a single exec_command call instead of leaving a process running to check ' +
'on later. SERVICE_TOKEN holds a credential: you may use it, but never print its value.',
defaultManifest: new Manifest({
environment: { SERVICE_TOKEN: workerEnvValue('SANDBOX_SERVICE_TOKEN') },
}),
});
const result = await new TemporalOpenAIRunner().run(agent, prompt, {
runConfig: { sandbox: { client: temporalSandboxClient('local') } },
});
if (typeof result.finalOutput !== 'string') {
throw ApplicationFailure.nonRetryable('SandboxAgent returned no final output');
}
return result.finalOutput;
}
Event History records every sandbox Activity's input and result, including the session's manifest and each command's
output, so a literal manifest value is recorded even when it's marked ephemeral: true. To keep a secret's value out
of the manifest, set the manifest value to workerEnvValue() with the name of a Worker environment variable, as the
Workflow above does, and list that name in the plugin's resolvableWorkerEnvVars. The manifest then records only the
variable's name. Unix-local and Docker also leave the resolved value out of the session state they report, so with
those backends the value doesn't enter Event History. With other backends, keeping it out of that state is the
backend's responsibility. Set the variable on every Worker that runs sandbox Activities, because a listed variable that
isn't set reads as an empty string.
workerEnvValue() doesn't stop a secret from reaching history another way. A command that prints the secret still
records it in Event History, so tell the agent not to print it, and encrypt what reaches history with a
Payload Codec.
The runner tears down the session when the run completes or fails, unless you passed it in runConfig.sandbox.session.
Close a session you pass that way yourself when you're done with it, by calling its stop(), shutdown(), and
delete() methods in the Workflow. A run that pauses for approval keeps its session, so resume it as described in
Keep conversation history and pause for approval and pass the
same runConfig.sandbox to that run() call.
Canceling, terminating, or timing out the Workflow skips the teardown, as does never resuming a paused run. Remove those sessions through your backend, for example by deleting the Docker container or the hosted sandbox.
Unix-local and Docker sessions stay on the machine that created them, so a Worker on another machine can't reach them.
With those backends, send sandbox Activities to a Task Queue that only one host's Workers poll, by passing Activity
options with that taskQueue as the second argument to temporalSandboxClient(). The options replace the default
five-minute Start-To-Close Timeout instead of merging with it, so include a startToCloseTimeout.
With Unix-local, let a single Worker process handle sandbox Activities and don't restart it during a run. A Unix-local session that another Worker process reconnects to, including the same Worker after a restart, goes back to the snapshot taken when the session was created or last paused and loses later changes. Neither Unix-local nor Docker can reconnect a process that's still running, which is why the Workflow above tells the agent to finish each command in one call. Failure behavior covers reconnects in detail, and the sandboxes sample shows this Worker and Workflow with the Client that starts them.
Stream model output
Pass { stream: true } to run() to stream model events to a client while the run stays durable. Streaming is
experimental. It publishes over Workflow Streams, so install
@temporalio/workflow-streams and construct a
WorkflowStream at the top of the Workflow. Set the topic in modelParams.streamingTopic on the Client's plugin, or in
the runner's defaultModelParams, and leave useLocalActivity off, since a streaming run fails with it.
Each model call runs as a streaming Activity that publishes the model's events to the topic as they arrive. In the Workflow, iterate the result to drive the run to completion. That iteration yields each call's events only after the call finishes, so subscribe from outside the Workflow to see them live. A retried model call publishes its events again from the start, so a subscriber can see a call's events more than once.
Catch errors from iterating the result and from awaiting result.completed, since the OpenAI Agents SDK's errors
arrive there as its own errors rather than as AgentsWorkflowError. An uncaught one doesn't fail the Workflow but fails
the Workflow Task, which retries.
Subscribe with WorkflowStreamClient, as described in
Subscribe, and keep the Workflow open until the subscriber
has the last event, as described in
Closing the stream. The
streaming sample shows the
Workflow, the Client's topic setting, and a subscriber that signals the Workflow when it's done.
Trace agent runs
OpenAI Agents SDK traces follow an agent run across the Client, the Workflow, Activities, Nexus Operations, and MCP calls. Spans from Workflow code go to the trace processors in the Worker process, so register exporters and processors there, not in Workflow code. Those spans aren't emitted again when the Worker replays history.
To send traces to OpenAI's dashboard, register OpenAI's exporter with
addTraceProcessor(new BatchTraceProcessor(new OpenAITracingExporter())). Register your own TracingProcessor with
addTraceProcessor too. Don't call setDefaultOpenAITracingExporter() after constructing the plugin, because it
replaces every trace processor, including the one the plugin uses for OpenTelemetry.
By default, OpenAI Agents SDK spans include model and tool inputs and outputs, and
runConfig.traceIncludeSensitiveData: false leaves them out, as described in
Sensitive data in OpenAI's tracing guide.
The SDK's sandbox.exec span records each sandbox command's text even then, so a command that contains a secret
reaches every trace processor.
To export OpenAI Agents SDK spans through OpenTelemetry, install the optional peer dependency
@opentelemetry/sdk-trace-base, then do the following
in both the Worker and the Client process:
- Create a tracer provider with
createTracerProvider()from@temporalio/openai-agents/otel, and add your exporter's span processor to it withaddSpanProcessor(). The provider starts with no span processor, so it exports nothing until you add one. - Before constructing the plugin, register the provider with
trace.setGlobalTracerProvider(). - Set
useOtelInstrumentation: truein the plugin'sinterceptorOptions.
If your application already has its own tracer provider, see Composing with other plugins.
Set addTemporalSpans: true in interceptorOptions to add temporal:* spans for Temporal operations, such as Workflow
starts, Signals, and Activities, and sandbox:* spans for sandbox operations. They're OpenAI Agents SDK spans, so they
reach every trace processor, and OpenTelemetry when it's on. A span for a Client call, and for the Workflow start,
Signal, Query, or Update it makes, appears only when that call runs inside an agent trace, such as one started with
withTrace().
Workflow code reads interceptorOptions from the Client that started the Workflow, the same way it reads modelParams,
so set them on the Client's plugin for spans from Workflow code. On the Worker's plugin, addTemporalSpans adds spans
for Activities. The
tracing sample shows a custom
processor, OpenAI's exporter, and OpenTelemetry.
Reference and troubleshooting
Feature support
| Feature | Support | Notes |
|---|---|---|
| Multi-turn agent runs | Supported | The agent loop runs in the Workflow. |
| Handoffs | Supported | Agent[] and handoff() forms, including input filters. |
| Function tools | Supported | tool() runs in the Workflow and must be deterministic. |
| Activities as tools | Supported | activityAsTool(). The Activity doesn't receive the run's context. |
| Nexus Operations as tools | Supported | nexusOperationAsTool(). |
| Agents as tools | Supported | agentAsTool(). |
| Hosted tools | Supported | The model provider runs them during the model Activity. |
| Hosted MCP tools | Supported | onApproval runs in the Workflow. |
| Stateless MCP servers | Supported | One connection per tool listing or tool call. |
| Stateful MCP servers | Supported | A dedicated Worker holds the server for each Workflow run. |
| MCP prompts | Not supported through MCP handles | Fetch prompts in your own Activity. |
| Sandbox agents | Supported (experimental) | Unix-local and Docker sessions are tied to one host. |
| Reconnecting to a sandbox process that's still running | Not supported on Unix-local or Docker | Have the agent finish each command in one exec_command call. See Failure behavior. |
| Sandbox runs paused for approval | Supported on backends with resume() | Every paused-run resume calls the backend's resume(). See Failure behavior. |
| Sessions | Supported | WorkflowSafeMemorySession. The SDK's MemorySession is rejected. |
| Run state and approvals | Supported | Serialize RunState to resume across Continue-As-New. |
| Guardrails | Supported | Run in the Workflow. Without stream: true, a tripwire arrives as the cause of an AgentsWorkflowError. |
| Structured output | Supported | outputType on the agent. |
| Run context | Supported | Visible to code in the Workflow, not to Activity tools or agentAsTool nested runs. |
| Per-run model override | Supported | runConfig.model takes a model name. |
| Model objects on agents | Not supported | Name the model with a string. |
| Custom model providers | Supported | Any ModelProvider, called in the model Activity. |
| Local Activities for model calls | Supported | useLocalActivity: true in the Client's modelParams or the runner's defaultModelParams. |
| Streaming | Supported (experimental) | Requires a streamingTopic and a WorkflowStream constructed in the Workflow. Not with Local Activities. |
| Tracing | Supported | OpenAI's exporter, custom processors, OpenTelemetry, and temporal:* spans. |
Cancellation through signal | Not supported | Cancel the Workflow or use a cancellation scope. Both skip a sandbox run's teardown. See Failure behavior. |
| Child Workflows and Continue-As-New | Supported | Plugin settings from the Client carry over. |
| Voice and realtime agents | Not supported |
Composing with other plugins
OpenAIAgentsPlugin adds its Activities and interceptors to the ones already on the Worker and Client, so it works
alongside other plugins. Register one instance per Client. When several are registered, the last one's modelParams and
interceptorOptions reach the Workflow.
The plugin's bundler hook puts its polyfills first in the Workflow bundle, ahead of every other module, so plugin order
doesn't affect them. When you prebuild the bundle with bundleWorkflowCode, pass the plugin to bundleWorkflowCode as
well as to the Worker, so the bundle includes the polyfills and the Workflow interceptor. To replay histories, as in
How to Replay a Workflow Execution, pass the plugin to
Worker.runReplayHistory too.
The plugin registers Activities named invokeModelActivity and invokeModelStreamActivity, plus Activities whose names
start with each MCP and sandbox provider's name. Don't register your own Activities under those names.
OpenTelemetry export needs a replay-safe global tracer provider. If your application already registers its own global
tracer provider, configure it with TemporalIdGenerator and mark it with markReplaySafeTracerProvider(). Mark only
providers that use TemporalIdGenerator.
Replay safety
These run in the Workflow and must be deterministic: the agent loop, agent instructions functions, tool() execute
functions, guardrails, handoff input filters, callModelInputFilter, hosted MCP onApproval callbacks,
WorkflowSafeMemorySession, a ModelSummaryProvider, and agentAsTool's customOutputExtractor. They can use
Workflow APIs such as Signals, condition(), and proxyActivities.
These run in Activities on the Worker: model calls through the model provider, hostedToolCredentials, Activities
wrapped with activityAsTool, MCP server operations, and sandbox operations. nexusOperationAsTool runs a Nexus
Operation.
Model requests and responses, tool arguments and results, MCP results, and sandbox inputs and results, including manifests, command output, and the session state a backend reports, are all Activity or Nexus inputs and results. Event History stores them, and replay reads them back instead of calling again. Encrypt sensitive data with a Payload Codec, and account for the payload size limit listed under Programming model level in the Temporal Cloud limits. For long conversations, use Continue-As-New before Event History approaches its limits.
Replay reproduces what the original run saw. It rebuilds WorkflowSafeMemorySession history, a streaming run iterates
the same events its streaming Activity returned, and trace and span identifiers come out the same because they come
from a replay-seeded random source. A Continue-As-New starts the session empty, but the plugin settings from the Client
carry over to the new run and to child Workflows.
Configuration
| Entry point | Use from | APIs |
|---|---|---|
@temporalio/openai-agents | Worker and Client code | OpenAIAgentsPlugin, StatelessMCPServerProvider, StatefulMCPServerProvider, SandboxClientProvider, OpenAIAgentsTraceClientInterceptor for wiring a Client by hand, DEDICATED_WORKER_FAILURE_TYPE, and option types |
@temporalio/openai-agents/workflow | Workflow code | TemporalOpenAIRunner, activityAsTool, nexusOperationAsTool, agentAsTool, statelessMcpServer, statefulMcpServer, WorkflowSafeMemorySession, temporalSandboxClient, workerEnvValue, ToolSerializationError, and DEDICATED_WORKER_FAILURE_TYPE |
@temporalio/openai-agents/otel | Worker and Client code | createTracerProvider, TemporalIdGenerator, markReplaySafeTracerProvider, and isReplaySafeTracerProvider. Requires @opentelemetry/sdk-trace-base. |
@temporalio/openai-agents/workflow-interceptor | Worker bundling | The Workflow interceptor module, for listing by hand in interceptors.workflowModules on Worker.create, or in workflowInterceptorModules for bundleWorkflowCode |
OpenAIAgentsPlugin options:
| Option | Set on | Default | Behavior |
|---|---|---|---|
modelProvider | Worker, and required on the Client | None | Resolves model names in the model Activity. The Client's plugin requires one but never calls it. |
modelParams | Client | None | Model Activity options sent to each Workflow the Client starts. See the next table. |
interceptorOptions.addTemporalSpans | Client and Worker | false | Adds temporal:* spans and sandbox:* spans. The Client covers Client calls and Workflow code, and the Worker covers Activities. See the notes below this table. |
interceptorOptions.useOtelInstrumentation | Client and Worker | false | On the Client, forwards spans from Workflow code to OpenTelemetry. On the Worker, it only makes the constructor check for a replay-safe tracer provider, since OpenAI Agents SDK spans from Activities reach the global tracer provider either way. Requires a replay-safe global tracer provider in that process before the plugin is constructed. |
mcpServerProviders | Worker | [] | Registers each MCP provider's Activities. |
sandboxClientProviders | Worker | [] | Registers each sandbox provider's Activities. Experimental. |
resolvableWorkerEnvVars | Worker | [] | Worker environment variable names that a workerEnvValue() may read. '*' allows every name. A listed variable that isn't set reads as an empty string. Experimental. |
hostedToolCredentials | Worker | None | A function that takes a hosted tool's identity and returns, or resolves to, its credentials. Returning undefined leaves the tool as the Workflow declared it. Experimental. See the notes below this table. |
Notes on addTemporalSpans:
- On the Client, it adds
temporal:*spans for Client calls and, in Workflow code, for Workflow starts, Signals, Queries, Updates, Activities, Local Activities, child Workflows, Nexus Operations, and Continue-As-New, plussandbox:*spans for sandbox operations. On the Worker, it addstemporal:executeActivity:*spans andsandbox:*:resultspans. - A span for a Client call, and for the Workflow start, Signal, Query, or Update it makes, appears only when that call runs inside an agent trace.
- The
sandbox:*spans carry only the session identifier and, depending on the operation, an exit code, a port, or a result size. Session creation gets nosandbox:*span and no:resultspan, and shows up only as the generictemporal:startActivity:*andtemporal:executeActivity:*spans for the provider's-sandbox-client-createActivity. - The SDK's own
sandbox.execspan is separate, isn't controlled by this option, and records the command text even withtraceIncludeSensitiveData: false.
Notes on hostedToolCredentials:
- The identity is
{ tool: 'hostedMcp', name, serverLabel, serverUrl?, connectorId? },{ tool: 'shell', name, allowedDomains }, or{ tool: 'codeInterpreter', name, allowedDomains }. The credentials are{ authorization?, headers?, domainSecrets? }, with each domain secret as{ domain, name, value }. - The model Activity calls the function on every model call for each hosted MCP tool with a server label, shell tool
with a
container_autodomain allowlist, and code interpreter with an inline container allowlist, so cache secrets it fetches. - It fills only fields the Workflow left out:
authorizationandheadersfor hosted MCP, anddomainSecretsfor shell and code interpreter. - The model Activity's result records the model provider's full response, so a credential the provider echoes back is recorded in Event History. The plugin doesn't strip it, since redacting the response is the model provider's responsibility.
- A throw fails the model Activity, and its message is recorded in Event History, so keep credentials out of it.
Model Activity options, in the Client's modelParams or the runner's defaultModelParams:
| Option | Default | Behavior |
|---|---|---|
ActivityOptions fields, such as startToCloseTimeout, scheduleToCloseTimeout, retry, taskQueue, and heartbeatTimeout | startToCloseTimeout: '60s' | Apply to every model Activity. With a heartbeatTimeout, the model Activity heartbeats on a timer at half that interval, which detects a lost Worker but not a stalled call. |
useLocalActivity | false | Runs model calls as Local Activities. Only startToCloseTimeout, scheduleToCloseTimeout, retry, cancellationType, and summary apply then. |
summary | None | The Activity summary shown in the Temporal UI. A string works anywhere. A ModelSummaryProvider, whose provide(agent, instructions, input) runs in the Workflow and must be deterministic, works only in defaultModelParams. |
streamingTopic | None | The Workflow Stream topic for streaming runs. Required with { stream: true }. Experimental. |
streamingBatchInterval | The Workflow Streams default | How often the streaming Activity flushes published events. Experimental. |
TemporalOpenAIRunner options:
| API | Option | Default | Behavior |
|---|---|---|---|
| Constructor | defaultModelParams | None | Model Activity options set in Workflow code, which also reach Workflows started without the plugin, such as by a Schedule or the Temporal CLI. The Client's modelParams take precedence for each field they set. They don't reach agentAsTool nested runs, which use only the Client's modelParams and the plugin's defaults. |
run() | context, maxTurns, previousResponseId, conversationId, session, sessionInputCallback, callModelInputFilter, tracing | SDK defaults | As in the SDK's run options. session can't be the SDK's MemorySession. Other SDK run options, such as signal, errorHandlers, toolErrorFormatter, and toolExecution, aren't accepted. |
run() | stream | false | Streams model events over a Workflow Stream. Experimental. |
run() | runConfig.model | Each agent's model | A model name used by every agent in the run. |
run() | runConfig.modelSettings, handoffInputFilter, inputGuardrails, outputGuardrails, tracingDisabled, traceIncludeSensitiveData, workflowName, traceId, groupId, traceMetadata | SDK defaults | As in the SDK's run config. Guardrails and filters run in the Workflow. |
run() | runConfig.sandbox | None | Required when a SandboxAgent is reachable from the agent through handoffs or an agentAsTool tool. client must come from temporalSandboxClient(). Experimental. See the notes below this table. |
Notes on runConfig.sandbox:
- It's passed on to
agentAsToolnested runs. - With a
SandboxAgentreached only through anagentAsTooltool, a missingrunConfig.sandboxon the parent run isn't caught up front. The nested run raisesSandboxConfigurationErrorwhen the tool is called, which ends the parent run with anAgentsWorkflowErrorthat carries it oncause.error. clientis required even withsession.- The other fields,
options,manifest,snapshot,session,sessionState,concurrencyLimits, andarchiveLimits, go to the OpenAI Agents SDK unchanged, as described in sandbox run options in OpenAI's sandbox agents concepts guide. options,manifest, andsnapshotgo into the create Activity's input, so Event History records them.
Tool, MCP, session, and sandbox APIs:
| API | Option | Default | Behavior |
|---|---|---|---|
activityAsTool(definition, options) | name, description, parameters | Required | name must match a registered Activity. parameters is a JSON schema object, and the type parameter doesn't check it against the Activity's input. |
activityAsTool | startToCloseTimeout, heartbeatTimeout, taskQueue, retryPolicy | startToCloseTimeout: '1 minute' | Activity options for the tool call. |
activityAsTool, nexusOperationAsTool | strict | true | Strict schema mode for the tool's arguments. The schema is sent as written, so with strict: true, list every property in required and set additionalProperties: false, or OpenAI's API rejects the request and the model Activity fails with ModelInvocationError.BadRequest. |
nexusOperationAsTool(operation, definition, options) | service, endpoint | Required | The Nexus service definition and the Nexus Endpoint name. definition takes the same fields as activityAsTool's. |
nexusOperationAsTool | scheduleToCloseTimeout | None | The Operation's Schedule-To-Close Timeout. |
agentAsTool(agent, options) | toolName | Required | The tool's name. The tool takes one string argument, input. The nested run gets only the parent's runConfig.sandbox. See the notes below this table. |
agentAsTool | toolDescription | '' | The tool's description. |
agentAsTool | customOutputExtractor | The final output, as JSON if it isn't a string | Turns the nested run's result into the tool's output. |
statelessMcpServer(name, options) | startToCloseTimeout, heartbeatTimeout, taskQueue, retryPolicy | startToCloseTimeout: '1 minute' | Activity options for listing and calling tools. |
statelessMcpServer, statefulMcpServer | cacheToolsList | true | Reuses the server's tool list after the first listing. Set it to false if the tools can change during a run. |
statelessMcpServer, statefulMcpServer | factoryArgument | None | Passed to the provider's factory on the Worker. |
statefulMcpServer | config | startToCloseTimeout: '1 minute', scheduleToStartTimeout: '30 seconds' | Activity options for tool operations, which also take heartbeatTimeout and retryPolicy. Operations always go to the run's dedicated Task Queue. |
statefulMcpServer | serverSessionConfig | startToCloseTimeout: '1 hour' | Start-To-Close and Heartbeat Timeouts for the Activity that holds the server open. Raise the timeout for runs that keep the server longer. |
StatelessMCPServerProvider(name, factory) | Arguments | Required | factory returns a new SDK MCP server for each operation. |
StatefulMCPServerProvider(name, factory, connection) | Arguments | Required | factory returns a new server for each Workflow run. The dedicated Worker connects through connection, runs in the Worker process that ran the server session Activity, and handles one tool operation at a time, so parallel tool calls to one stateful server run one after another. |
WorkflowSafeMemorySession(options) | sessionId | The Workflow ID | The session identifier. |
WorkflowSafeMemorySession | initialItems | [] | History to start from, such as the previous run's items. |
SandboxClientProvider(name, client) | Arguments | Required | Wraps an SDK sandbox client. Its Activities are named after name. |
temporalSandboxClient(name, options) | options | { startToCloseTimeout: '5 minutes' } | Activity options for every sandbox operation. They replace the default instead of merging with it. |
workerEnvValue(name) | Arguments | Required | A manifest environment value that the Worker reads from its environment variable name. |
Notes on agentAsTool nested runs:
- The nested run is started with the parent's
runConfig.sandboxand nothing else from the parent'srun()call. - It doesn't get the parent's
context, the parent's other run options (maxTurns,previousResponseId,conversationId,session,sessionInputCallback,callModelInputFilter, andtracing), or any otherrunConfigfield (model,modelSettings,handoffInputFilter,inputGuardrails,outputGuardrails,tracingDisabled,traceIncludeSensitiveData,workflowName,traceId,groupId, andtraceMetadata). These all take their SDK defaults, andagentAsToolhas no option to pass them. - Its model Activities use the Client's
modelParamsbut not the parent runner'sdefaultModelParams, so a field the Client didn't set takes the plugin's default, a 60-second Start-To-Close Timeout on a regular Activity.
Providers are matched to Workflow handles by name, and each provider on a plugin needs a unique name. The plugin adds a
-stateful suffix to a stateful MCP provider's name, so a stateless and a stateful MCP provider can share a name.
Failure behavior
Which error reaches your Workflow depends on where the failure starts. A failed model call, MCP tool listing, or sandbox
session operation, such as creating or resuming the session, reaches the Workflow as an ActivityFailure with the
Activity's error as its cause, once its Activity stops retrying. A failed activityAsTool, nexusOperationAsTool, or
agentAsTool call ends the run with an AgentsWorkflowError, as described next. Failures that the table below lists as
raised in the Workflow reach it as they are when they occur outside a tool call.
A failed activityAsTool or agentAsTool call ends the run, and so does a failed nexusOperationAsTool call, which
builds its tool the same way. In a run without stream: true, the failure reaches the Workflow as a non-retryable
AgentsWorkflowError whose cause is the OpenAI Agents SDK's ToolCallError. The original failure is on that
ToolCallError's error property, not on the cause chain, so read it from cause.error. It's the ActivityFailure
once an activityAsTool Activity stops retrying, the NexusOperationFailure from nexusOperationAsTool, or, from
agentAsTool, the nested run's own failure, such as a NestedAgentInterruption, SandboxConfigurationError, or
AgentsWorkflowError. The Workflow failure recorded in Event History keeps only the message text, so an
activityAsTool Activity's own error appears only in that Activity's failure event.
A failure inside an MCP tool call or a sandbox tool, such as exec_command, write_stdin, view_image, or
apply_patch, doesn't reach the Workflow. The OpenAI Agents SDK returns it to the model as the tool's output, such as
An error occurred while running the tool. Please try again. Error: ActivityFailure: Activity task failed, without the
underlying error, and the run continues. That includes the DedicatedWorkerFailure, StatefulMCPServerNotFound, and
SandboxOperationUnsupported errors below when an MCP or sandbox tool call raises them.
| Failure type | Raised in | Meaning |
|---|---|---|
ModelInvocationError, and ModelInvocationError.RateLimit, .Authentication, .BadRequest, .Timeout, .Conflict, or .ServerError | Model Activity | The model call failed. HTTP 408, 409, 429, and 5xx responses are retryable, and other statuses aren't. An x-should-retry header overrides that, and retry-after-ms or retry-after sets the next retry delay. An openai APIError without an HTTP status, such as APIConnectionError, is non-retryable. Other errors are retryable. |
AgentsWorkflowError | Workflow | Non-retryable. An agent names its model with a model object instead of a string, an agent's tools holds a raw function, or the OpenAI Agents SDK threw during a run without stream: true, with its error as the cause. A failed activityAsTool, nexusOperationAsTool, or agentAsTool call in such a run also arrives this way, with the SDK's ToolCallError as the cause and the original failure on cause.error. In a streaming run, an error the SDK throws after run() returns isn't wrapped, and iterating the result or awaiting result.completed throws it as it is, so an uncaught one fails the Workflow Task instead of the Workflow. |
UnsafeSessionError | Workflow | Non-retryable. run() received the SDK's MemorySession. |
StreamingTopicNotConfigured | Workflow | Non-retryable. A streaming run has no streamingTopic. |
StreamingLocalActivityUnsupported | Workflow | Non-retryable. A streaming run has useLocalActivity: true. |
NestedAgentInterruption | Workflow | Non-retryable. An agentAsTool nested run paused for approval. It ends the parent run and reaches the Workflow on cause.error of an AgentsWorkflowError. |
DedicatedWorkerFailure, exported as DEDICATED_WORKER_FAILURE_TYPE | Workflow | Retryable. A stateful MCP operation wasn't picked up within its Schedule-To-Start Timeout or missed a heartbeat, so the run's dedicated Worker isn't polling. |
StatefulMCPServerAlreadyRunning | Server session Activity | Non-retryable. Two stateful handles with the same name connected in one Workflow run. |
StatefulMCPServerNotFound | Dedicated Worker | Non-retryable. A tool operation reached a Worker that doesn't hold the server. |
SandboxConfigurationError | Workflow | Non-retryable. A SandboxAgent is reachable but runConfig.sandbox is missing, or its client didn't come from temporalSandboxClient(). It's also raised when a manifest environment value uses a resolver function, directly or through resolve, or is an EnvValueReference that can't be serialized, since neither can travel in a sandbox Activity's input. Use workerEnvValue() for a value the Worker supplies. |
SandboxOperationUnsupported | Sandbox Activity | Non-retryable. The backend lacks the operation. A Worker that doesn't hold the session calls resume() before handling one of its operations, and every paused-run resume calls it too, so on a backend without resume(), the run() call that resumes a paused sandbox run fails. |
WorkerEnvValueError | Sandbox Activity | Non-retryable. A workerEnvValue() names a variable that isn't in resolvableWorkerEnvVars. |
SandboxSessionStateInvalid | Workflow | Non-retryable. Sandbox session state being resumed wasn't produced by a Temporal sandbox client. |
The sandbox error's code | Sandbox Activity | Non-retryable. The OpenAI Agents SDK marked the sandbox error as not retryable. Other sandbox errors are retried. |
The OpenAI Agents SDK parses a tool call's arguments before the tool runs, so ToolSerializationError isn't thrown
during a run. When the arguments for an activityAsTool, nexusOperationAsTool, or agentAsTool tool aren't valid
JSON, the run pauses as if the tool needed approval: run() returns with that call in result.interruptions and no
finalOutput. Approving it sends the model a parse error instead of running the tool.
Calling a stateful MCP handle's tools before connect() throws an ApplicationFailure that fails the Workflow. The
plugin constructor throws if two of its providers share a name, if useOtelInstrumentation is set without a
replay-safe global tracer provider, or if one SandboxClientProvider is passed to plugins with different
resolvableWorkerEnvVars.
Sandbox session lifecycle. Creating a session isn't idempotent, so a create Activity retried after the backend made
the session leaves that session behind. A session is also left running when a
cancellation scope stops run(), since the runner skips the
teardown even if the Workflow then completes normally. Remove those sessions through your backend. The runner's teardown
calls the session's stop(), shutdown(), and delete(), each as an Activity, but only for a session it created or
resumed, so a session passed in runConfig.sandbox.session stays open after the run completes or fails.
Sandbox reconnects. Any Worker polling the Task Queue can receive a sandbox operation. A Worker that doesn't hold
the session, such as a restarted Worker, reconnects to it with the backend's resume(), and resuming a paused run
reconnects the same way, even on the Worker that ran it before the pause. What resume() restores depends on the
backend, as described in
Resume and snapshots in
OpenAI's sandbox clients guide.
With Unix-local and Docker, an operation that reaches a Worker on another machine fails, and Temporal retries it. A Docker session keeps its workspace across a reconnect while its container is running. On both backends, resuming a paused run keeps the workspace as it was at the pause.
Sandbox processes. If a command is still running when exec_command returns, the agent gets a process identifier
to pass to write_stdin later. On Unix-local and Docker, process identifiers start over after a reconnect, including
when a paused run resumes. A write_stdin with an earlier identifier either reports session not found or reaches a
different process that the agent started after the reconnect. A process started before a pause keeps running, on
Unix-local even after the session is torn down. exec_command returns when the command finishes or when
yield_time_ms passes, so tell the agent to raise it for slower commands, up to a limit below the sandbox Activity's
Start-To-Close Timeout.