Skip to main content

OpenAI Agents SDK integration

View Markdown

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:

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.

info

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: StatelessMCPServerProvider takes a name and a factory that returns an OpenAI Agents SDK MCP server, such as MCPServerStdio or MCPServerStreamableHttp. In the Workflow, use statelessMcpServer(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: StatefulMCPServerProvider also takes a NativeConnection, usually the Worker's. In the Workflow, call connect() on the statefulMcpServer(name) handle before the run and cleanup() in a finally block. 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 until cleanup().

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'],
}),
],
danger

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:

  1. Create a tracer provider with createTracerProvider() from @temporalio/openai-agents/otel, and add your exporter's span processor to it with addSpanProcessor(). The provider starts with no span processor, so it exports nothing until you add one.
  2. Before constructing the plugin, register the provider with trace.setGlobalTracerProvider().
  3. Set useOtelInstrumentation: true in the plugin's interceptorOptions.

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​

FeatureSupportNotes
Multi-turn agent runsSupportedThe agent loop runs in the Workflow.
HandoffsSupportedAgent[] and handoff() forms, including input filters.
Function toolsSupportedtool() runs in the Workflow and must be deterministic.
Activities as toolsSupportedactivityAsTool(). The Activity doesn't receive the run's context.
Nexus Operations as toolsSupportednexusOperationAsTool().
Agents as toolsSupportedagentAsTool().
Hosted toolsSupportedThe model provider runs them during the model Activity.
Hosted MCP toolsSupportedonApproval runs in the Workflow.
Stateless MCP serversSupportedOne connection per tool listing or tool call.
Stateful MCP serversSupportedA dedicated Worker holds the server for each Workflow run.
MCP promptsNot supported through MCP handlesFetch prompts in your own Activity.
Sandbox agentsSupported (experimental)Unix-local and Docker sessions are tied to one host.
Reconnecting to a sandbox process that's still runningNot supported on Unix-local or DockerHave the agent finish each command in one exec_command call. See Failure behavior.
Sandbox runs paused for approvalSupported on backends with resume()Every paused-run resume calls the backend's resume(). See Failure behavior.
SessionsSupportedWorkflowSafeMemorySession. The SDK's MemorySession is rejected.
Run state and approvalsSupportedSerialize RunState to resume across Continue-As-New.
GuardrailsSupportedRun in the Workflow. Without stream: true, a tripwire arrives as the cause of an AgentsWorkflowError.
Structured outputSupportedoutputType on the agent.
Run contextSupportedVisible to code in the Workflow, not to Activity tools or agentAsTool nested runs.
Per-run model overrideSupportedrunConfig.model takes a model name.
Model objects on agentsNot supportedName the model with a string.
Custom model providersSupportedAny ModelProvider, called in the model Activity.
Local Activities for model callsSupporteduseLocalActivity: true in the Client's modelParams or the runner's defaultModelParams.
StreamingSupported (experimental)Requires a streamingTopic and a WorkflowStream constructed in the Workflow. Not with Local Activities.
TracingSupportedOpenAI's exporter, custom processors, OpenTelemetry, and temporal:* spans.
Cancellation through signalNot supportedCancel the Workflow or use a cancellation scope. Both skip a sandbox run's teardown. See Failure behavior.
Child Workflows and Continue-As-NewSupportedPlugin settings from the Client carry over.
Voice and realtime agentsNot 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 pointUse fromAPIs
@temporalio/openai-agentsWorker and Client codeOpenAIAgentsPlugin, StatelessMCPServerProvider, StatefulMCPServerProvider, SandboxClientProvider, OpenAIAgentsTraceClientInterceptor for wiring a Client by hand, DEDICATED_WORKER_FAILURE_TYPE, and option types
@temporalio/openai-agents/workflowWorkflow codeTemporalOpenAIRunner, activityAsTool, nexusOperationAsTool, agentAsTool, statelessMcpServer, statefulMcpServer, WorkflowSafeMemorySession, temporalSandboxClient, workerEnvValue, ToolSerializationError, and DEDICATED_WORKER_FAILURE_TYPE
@temporalio/openai-agents/otelWorker and Client codecreateTracerProvider, TemporalIdGenerator, markReplaySafeTracerProvider, and isReplaySafeTracerProvider. Requires @opentelemetry/sdk-trace-base.
@temporalio/openai-agents/workflow-interceptorWorker bundlingThe Workflow interceptor module, for listing by hand in interceptors.workflowModules on Worker.create, or in workflowInterceptorModules for bundleWorkflowCode

OpenAIAgentsPlugin options:

OptionSet onDefaultBehavior
modelProviderWorker, and required on the ClientNoneResolves model names in the model Activity. The Client's plugin requires one but never calls it.
modelParamsClientNoneModel Activity options sent to each Workflow the Client starts. See the next table.
interceptorOptions.addTemporalSpansClient and WorkerfalseAdds 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.useOtelInstrumentationClient and WorkerfalseOn 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.
mcpServerProvidersWorker[]Registers each MCP provider's Activities.
sandboxClientProvidersWorker[]Registers each sandbox provider's Activities. Experimental.
resolvableWorkerEnvVarsWorker[]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.
hostedToolCredentialsWorkerNoneA 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, plus sandbox:* spans for sandbox operations. On the Worker, it adds temporal:executeActivity:* spans and sandbox:*:result spans.
  • 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 no sandbox:* span and no :result span, and shows up only as the generic temporal:startActivity:* and temporal:executeActivity:* spans for the provider's -sandbox-client-create Activity.
  • The SDK's own sandbox.exec span is separate, isn't controlled by this option, and records the command text even with traceIncludeSensitiveData: 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_auto domain allowlist, and code interpreter with an inline container allowlist, so cache secrets it fetches.
  • It fills only fields the Workflow left out: authorization and headers for hosted MCP, and domainSecrets for 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:

OptionDefaultBehavior
ActivityOptions fields, such as startToCloseTimeout, scheduleToCloseTimeout, retry, taskQueue, and heartbeatTimeoutstartToCloseTimeout: '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.
useLocalActivityfalseRuns model calls as Local Activities. Only startToCloseTimeout, scheduleToCloseTimeout, retry, cancellationType, and summary apply then.
summaryNoneThe 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.
streamingTopicNoneThe Workflow Stream topic for streaming runs. Required with { stream: true }. Experimental.
streamingBatchIntervalThe Workflow Streams defaultHow often the streaming Activity flushes published events. Experimental.

TemporalOpenAIRunner options:

APIOptionDefaultBehavior
ConstructordefaultModelParamsNoneModel 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, tracingSDK defaultsAs 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()streamfalseStreams model events over a Workflow Stream. Experimental.
run()runConfig.modelEach agent's modelA model name used by every agent in the run.
run()runConfig.modelSettings, handoffInputFilter, inputGuardrails, outputGuardrails, tracingDisabled, traceIncludeSensitiveData, workflowName, traceId, groupId, traceMetadataSDK defaultsAs in the SDK's run config. Guardrails and filters run in the Workflow.
run()runConfig.sandboxNoneRequired 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 agentAsTool nested runs.
  • With a SandboxAgent reached only through an agentAsTool tool, a missing runConfig.sandbox on the parent run isn't caught up front. The nested run raises SandboxConfigurationError when the tool is called, which ends the parent run with an AgentsWorkflowError that carries it on cause.error.
  • client is required even with session.
  • The other fields, options, manifest, snapshot, session, sessionState, concurrencyLimits, and archiveLimits, go to the OpenAI Agents SDK unchanged, as described in sandbox run options in OpenAI's sandbox agents concepts guide.
  • options, manifest, and snapshot go into the create Activity's input, so Event History records them.

Tool, MCP, session, and sandbox APIs:

APIOptionDefaultBehavior
activityAsTool(definition, options)name, description, parametersRequiredname must match a registered Activity. parameters is a JSON schema object, and the type parameter doesn't check it against the Activity's input.
activityAsToolstartToCloseTimeout, heartbeatTimeout, taskQueue, retryPolicystartToCloseTimeout: '1 minute'Activity options for the tool call.
activityAsTool, nexusOperationAsToolstricttrueStrict 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, endpointRequiredThe Nexus service definition and the Nexus Endpoint name. definition takes the same fields as activityAsTool's.
nexusOperationAsToolscheduleToCloseTimeoutNoneThe Operation's Schedule-To-Close Timeout.
agentAsTool(agent, options)toolNameRequiredThe 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.
agentAsTooltoolDescription''The tool's description.
agentAsToolcustomOutputExtractorThe final output, as JSON if it isn't a stringTurns the nested run's result into the tool's output.
statelessMcpServer(name, options)startToCloseTimeout, heartbeatTimeout, taskQueue, retryPolicystartToCloseTimeout: '1 minute'Activity options for listing and calling tools.
statelessMcpServer, statefulMcpServercacheToolsListtrueReuses the server's tool list after the first listing. Set it to false if the tools can change during a run.
statelessMcpServer, statefulMcpServerfactoryArgumentNonePassed to the provider's factory on the Worker.
statefulMcpServerconfigstartToCloseTimeout: '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.
statefulMcpServerserverSessionConfigstartToCloseTimeout: '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)ArgumentsRequiredfactory returns a new SDK MCP server for each operation.
StatefulMCPServerProvider(name, factory, connection)ArgumentsRequiredfactory 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)sessionIdThe Workflow IDThe session identifier.
WorkflowSafeMemorySessioninitialItems[]History to start from, such as the previous run's items.
SandboxClientProvider(name, client)ArgumentsRequiredWraps 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)ArgumentsRequiredA 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.sandbox and nothing else from the parent's run() call.
  • It doesn't get the parent's context, the parent's other run options (maxTurns, previousResponseId, conversationId, session, sessionInputCallback, callModelInputFilter, and tracing), or any other runConfig field (model, modelSettings, handoffInputFilter, inputGuardrails, outputGuardrails, tracingDisabled, traceIncludeSensitiveData, workflowName, traceId, groupId, and traceMetadata). These all take their SDK defaults, and agentAsTool has no option to pass them.
  • Its model Activities use the Client's modelParams but not the parent runner's defaultModelParams, 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 typeRaised inMeaning
ModelInvocationError, and ModelInvocationError.RateLimit, .Authentication, .BadRequest, .Timeout, .Conflict, or .ServerErrorModel ActivityThe 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.
AgentsWorkflowErrorWorkflowNon-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.
UnsafeSessionErrorWorkflowNon-retryable. run() received the SDK's MemorySession.
StreamingTopicNotConfiguredWorkflowNon-retryable. A streaming run has no streamingTopic.
StreamingLocalActivityUnsupportedWorkflowNon-retryable. A streaming run has useLocalActivity: true.
NestedAgentInterruptionWorkflowNon-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_TYPEWorkflowRetryable. 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.
StatefulMCPServerAlreadyRunningServer session ActivityNon-retryable. Two stateful handles with the same name connected in one Workflow run.
StatefulMCPServerNotFoundDedicated WorkerNon-retryable. A tool operation reached a Worker that doesn't hold the server.
SandboxConfigurationErrorWorkflowNon-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.
SandboxOperationUnsupportedSandbox ActivityNon-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.
WorkerEnvValueErrorSandbox ActivityNon-retryable. A workerEnvValue() names a variable that isn't in resolvableWorkerEnvVars.
SandboxSessionStateInvalidWorkflowNon-retryable. Sandbox session state being resumed wasn't produced by a Temporal sandbox client.
The sandbox error's codeSandbox ActivityNon-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.

Resources​