Strands Agents integration
Temporal's integration with Strands Agents is an SDK Plugin that gives your Strands agents Durable Execution via the Temporal platform. The plugin routes model invocations, tool calls, MCP tool calls, and hooks through Temporal Activities, so every step your agent takes is recorded in Workflow history and can survive crashes, restarts, and infrastructure failures.
The Temporal TypeScript SDK integration with Strands Agents is currently at an experimental release stage. The API may change in future versions.
Code snippets in this guide are taken from the Strands Agents plugin samples. Refer to the samples for the complete code.
Get started
Install the plugin, then run a minimal Strands agent inside a Temporal Workflow.
Prerequisites
- This guide assumes you are already familiar with Strands Agents. If you are not, refer to the Strands Agents documentation for more details.
- If you are new to Temporal, read Understanding Temporal or take the Temporal 101 course.
- Set up your local development environment by following the Set up your local with the TypeScript SDK guide. Leave the Temporal development server running if you want to test your code locally.
Install the plugin
Install the Strands Agents plugin alongside the Strands Agents SDK:
npm install @temporalio/strands-agents @strands-agents/sdk
Run a Strands agent with Durable Execution
The following example runs a Strands agent inside a Temporal Workflow. Model calls execute as Temporal Activities, which means they get automatic retries, timeouts, and durable execution. If the Worker process crashes mid-conversation, Temporal replays the Workflow and resumes from the last completed Activity.
1. Define the Workflow
Create a Workflow that constructs a TemporalAgent and invokes it with a prompt. The startToCloseTimeout in
activityOptions sets the maximum time each model call Activity can run:
strands-agents/src/workflows/hello-world.ts
import { TemporalAgent } from '@temporalio/strands-agents';
export async function helloWorld(prompt: string): Promise<string> {
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
});
const result = await agent.invoke(prompt);
return result.toString();
}
2. Start a Worker
Create a Worker that registers your Workflows and the StrandsPlugin. The plugin automatically registers the Activities
that handle model calls. The same Worker serves every example in this guide; the mcpClients wiring is explained in
Connect to MCP servers:
import path from 'node:path';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { McpClient } from '@strands-agents/sdk';
import { StrandsPlugin } from '@temporalio/strands-agents';
import { NativeConnection, Worker } from '@temporalio/worker';
import * as activities from './activities';
const ECHO_SERVER = path.join(__dirname, 'mcp-server.ts');
function makeEchoClient(): McpClient {
return new McpClient({
transport: new StdioClientTransport({
command: 'npx',
args: ['ts-node', ECHO_SERVER],
}),
});
}
async function run() {
const connection = await NativeConnection.connect({
address: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233',
});
try {
const worker = await Worker.create({
connection,
taskQueue: 'strands-agents',
workflowsPath: require.resolve('./workflows'),
activities,
// Omit `models:` so the plugin registers its default `BedrockModel` under
// the name `bedrock`. To use a different provider or pin a model ID,
// pass e.g. `models: { bedrock: () => new BedrockModel({ modelId: '...' }) }`.
plugins: [new StrandsPlugin({ mcpClients: { echo: makeEchoClient } })],
});
console.log('Worker started. Ctrl+C to exit.');
await worker.run();
} finally {
await connection.close();
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
3. Run the Workflow
Start the Workflow from a separate client script. This example sends the prompt "Write a haiku about durable execution" and prints the agent's response:
strands-agents/src/hello-world.ts
import { Client, Connection } from '@temporalio/client';
import { helloWorld } from './workflows';
async function run() {
const connection = await Connection.connect({
address: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233',
});
const client = new Client({ connection });
const result = await client.workflow.execute(helloWorld, {
args: ['Write a haiku about durable execution.'],
taskQueue: 'strands-agents',
workflowId: 'strands-hello-world',
});
console.log(`Result: ${result}`);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Build the agent
Customize which model provider your agent uses, add tools that run as Activities, subscribe to lifecycle events with hooks, and connect to MCP servers.
Choose and configure models
new StrandsPlugin({ models }) takes a mapping of name to factory function. Each factory is called lazily on first
use (on the Worker, outside the Workflow sandbox) and the constructed model is cached for the Worker's lifetime. If you
omit models, the plugin registers a single BedrockModel factory under the name "bedrock", matching Strands' own
implicit default.
When you provide a custom models mapping, each TemporalAgent selects which factory to invoke by name with the
model option:
import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
import { TemporalAgent, StrandsPlugin } from '@temporalio/strands-agents';
// workflow
export async function multiModelWorkflow(prompt: string): Promise<string> {
const a = new TemporalAgent({
model: 'claude',
activityOptions: { startToCloseTimeout: '60 seconds' },
});
const b = new TemporalAgent({
model: 'bedrock',
activityOptions: { startToCloseTimeout: '60 seconds' },
});
// ...
}
// worker
new StrandsPlugin({
models: {
claude: () => new AnthropicModel({ apiKey: '...' }),
bedrock: () => new BedrockModel({}),
},
});
Each TemporalAgent carries its own Activity options (timeouts, retry policy, task queue, streaming topic) and
dispatches to a shared model Activity, which resolves the model name against the registered factories at runtime. A
model name not present in the models mapping throws inside the Activity.
Run non-deterministic tools as Activities
Strands tools that perform I/O, access external services, or produce non-deterministic results need to run as Temporal
Activities rather than inline in the Workflow. Register the tool as an Activity on the Worker, and pass it to the agent
using workflow.activityAsTool. Deterministic tools can run directly in the Workflow as a plain Strands tool().
Define an Activity for the tool:
strands-agents/src/activities/tools.ts
export async function fetchWeather(input: { city: string }): Promise<{ city: string; temperatureF: number; conditions: string }> {
return {
city: input.city,
temperatureF: 72,
conditions: 'sunny',
};
}
Pass the Activity to the agent in the Workflow using workflow.activityAsTool (imported here as strandsWorkflow). The
inputSchema is a JSON Schema (or a Zod schema) that tells the model how to call the tool:
strands-agents/src/workflows/tools.ts
import { tool } from '@strands-agents/sdk';
import { TemporalAgent, workflow as strandsWorkflow } from '@temporalio/strands-agents';
import { z } from 'zod';
const letterCounter = tool({
name: 'letterCounter',
description: 'Count how many times `letter` appears in `word` (case-insensitive).',
inputSchema: z.object({
word: z.string(),
letter: z.string(),
}),
callback: ({ word, letter }) => word.toLowerCase().split(letter.toLowerCase()).length - 1,
});
export async function toolsWorkflow(prompt: string): Promise<string> {
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
tools: [
letterCounter,
strandsWorkflow.activityAsTool('fetchWeather', {
description: 'Fetch the current weather for a city.',
inputSchema: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
activityOptions: { startToCloseTimeout: '30 seconds', retry: { maximumAttempts: 3 } },
}),
],
});
const result = await agent.invoke(prompt);
return result.toString();
}
Register the Activity functions on the Worker by passing them in the activities option, as shown in
Start a Worker. The activityName passed to activityAsTool must match
the name the Activity is registered under.
React to agent lifecycle events
Strands' hook system lets you subscribe callbacks to events in the agent lifecycle, such as invocation start/end, model call before/after, tool call before/after, and message added. Use hooks to add logging, metrics, or custom logic at each stage.
Register callbacks with agent.addHook(EventClass, callback). Hook callbacks fire in Workflow context, so deterministic
callbacks work without any extra setup. For callbacks that need I/O (audit logging, metrics, alerting), use
workflow.activityAsHook to dispatch the work as a Temporal Activity. The following example shows both patterns. The
first callback mutates Workflow state (deterministic), while persistToolCall runs as an Activity (I/O-safe):
strands-agents/src/workflows/hooks.ts
import { AfterToolCallEvent, tool } from '@strands-agents/sdk';
import { TemporalAgent, workflow as strandsWorkflow } from '@temporalio/strands-agents';
import { z } from 'zod';
const echo = tool({
name: 'echo',
description: 'Echo back the input text.',
inputSchema: z.object({ text: z.string() }),
callback: ({ text }) => text,
});
export async function hooksWorkflow(prompt: string): Promise<string[]> {
const fired: string[] = [];
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
tools: [echo],
});
// Callback 1: in-workflow, deterministic state mutation.
agent.addHook(AfterToolCallEvent, (event) => {
fired.push(event.toolUse.name);
});
// Callback 2: dispatch to a Temporal activity for I/O.
agent.addHook(
AfterToolCallEvent,
strandsWorkflow.activityAsHook('persistToolCall', {
activityInput: (event) => event.toolUse.name,
activityOptions: { startToCloseTimeout: '15 seconds', retry: { maximumAttempts: 3 } },
})
);
await agent.invoke(prompt);
return fired;
}
The Activity dispatched by the hook is a normal Temporal Activity registered on the Worker:
strands-agents/src/activities/hooks.ts
import { log } from '@temporalio/activity';
export async function persistToolCall(toolName: string): Promise<void> {
// In production, write to a database / S3 / your audit pipeline.
log.info(`audit: tool ${toolName} completed`);
}
Hook callbacks run in Workflow context, so they must be
deterministic. Do not use Date.now(),
randomUUID(), or I/O inside hook callbacks. Use workflow.activityAsHook for anything that requires I/O.
The activityInput function extracts serializable values from the event to pass as the Activity's input. This is needed
because hook events hold references to the Agent, Tool instances, and other objects that cannot cross the Activity
boundary.
Connect to MCP servers
If your agent needs access to tools provided by an MCP server, configure the MCP clients on the Worker and reference them by name in the Workflow.
new StrandsPlugin({ mcpClients }) takes a mapping of name to McpClient factory, mirroring the models pattern. The
plugin registers per-server {name}-listTools and {name}-callTool Activities. In the Workflow,
new TemporalMCPClient({ server: 'name' }) is a thin handle that references the server by name and carries the per-call
Activity options.
Define the Workflow with a TemporalMCPClient:
strands-agents/src/workflows/mcp.ts
import { TemporalAgent, TemporalMCPClient } from '@temporalio/strands-agents';
export async function mcpWorkflow(prompt: string): Promise<string> {
const echo = new TemporalMCPClient({
server: 'echo',
activityOptions: { startToCloseTimeout: '30 seconds', retry: { maximumAttempts: 3 } },
});
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
tools: [echo],
});
const result = await agent.invoke(prompt);
return result.toString();
}
Register the MCP client factory on the Worker via mcpClients, as shown in the
Worker above. Each factory returns a fully configured McpClient, so you
can pass any options the McpClient constructor accepts (transport, URL, headers, and so on).
By default, TemporalMCPClient re-lists the server's tools on every agent turn, so an MCP server that is restarted or
redeployed mid-Workflow — with tools added, removed, or renamed — is picked up. To list the tools just once at the
beginning of the Workflow and reuse that schema for the Workflow's lifetime (one fewer Activity per turn), set
cacheTools: true:
const echo = new TemporalMCPClient({
server: 'echo',
cacheTools: true,
activityOptions: { startToCloseTimeout: '30 seconds' },
});
To amortize connection setup, the {name}-listTools and {name}-callTool Activities share one Worker-process MCP
connection and reuse it across calls. The connection is disconnected after it sits idle for mcpConnectionIdleTimeout
(default 5 minutes); the timer resets on every reuse. mcpConnectionIdleTimeout accepts a millisecond number or a
duration string (such as '30 seconds'), like startToCloseTimeout:
new StrandsPlugin({
mcpClients: { echo: () => new McpClient({ url: 'http://localhost:8765/mcp' }) },
mcpConnectionIdleTimeout: '30 seconds',
});
Interact with the agent
Control the shape of agent responses, stream output in real time, and pause the agent for human approval.
Add human approval gates
Some agent actions, such as deleting resources or sending messages, may require human approval before proceeding. Strands offers two ways to interrupt an agent and wait for a response. Both work with the plugin.
In each case, agent.invoke() returns an AgentResult with stopReason: 'interrupt' and an interrupts array instead
of throwing. Pair this with a Signal handler that supplies responses, then resume by calling agent.invoke(responses).
Interrupt from a hook
A hook on an interruptible event such as BeforeToolCallEvent can pause the agent by calling event.interrupt(...).
The hook runs in Workflow context, so it must be deterministic. The Workflow waits for a Signal carrying the approval
response, then resumes the agent:
strands-agents/src/workflows/human-in-the-loop.ts
import {
BeforeToolCallEvent,
tool,
type InterruptResponseContent,
type InterruptResponseContentData,
} from '@strands-agents/sdk';
import { TemporalAgent } from '@temporalio/strands-agents';
import { condition, defineQuery, defineSignal, setHandler } from '@temporalio/workflow';
import { z } from 'zod';
export const hitlApproveSignal = defineSignal<[string]>('hitlApprove');
export const hitlPendingApprovalQuery = defineQuery<string | null>('hitlPendingApproval');
const deleteFile = tool({
name: 'deleteFile',
description: 'Delete a file at the given path.',
inputSchema: z.object({ path: z.string() }),
callback: ({ path }) => `deleted ${path}`,
});
export async function humanInTheLoop(prompt: string): Promise<string> {
let approval: string | null = null;
let pendingReason: string | null = null;
setHandler(hitlApproveSignal, (response) => {
approval = response;
});
setHandler(hitlPendingApprovalQuery, () => pendingReason);
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
tools: [deleteFile],
});
agent.addHook(BeforeToolCallEvent, (event) => {
if (event.toolUse.name !== 'deleteFile') return;
const path = (event.toolUse.input as { path?: string }).path;
const response = event.interrupt<string>({
name: 'approval',
reason: `approve delete of ${path}?`,
});
if (response !== 'approve') {
event.cancel = 'denied';
}
});
let result = await agent.invoke(prompt);
while (result.stopReason === 'interrupt') {
const interrupts = result.interrupts ?? [];
pendingReason = (interrupts[0]?.reason as string | undefined) ?? null;
await condition(() => approval !== null);
const response = approval!;
approval = null;
pendingReason = null;
const responses: InterruptResponseContentData[] = interrupts.map((i) => ({
type: 'interruptResponse',
interruptResponse: { interruptId: i.id, response },
}));
result = await agent.invoke(responses as InterruptResponseContent[]);
}
return result.toString();
}
Interrupt from an activity tool
An activityAsTool-wrapped Activity can interrupt the agent by throwing an interrupt-shaped ApplicationFailure. The
plugin's failure converter preserves the interrupt payload across the Activity boundary, so AgentResult.interrupts is
populated the same way as for hooks.
Define the Activity that raises the interrupt with the STRANDS_INTERRUPT_TYPE failure type:
strands-agents/src/activities/activity-interrupt.ts
import { ApplicationFailure } from '@temporalio/common';
import { STRANDS_INTERRUPT_TYPE } from '@temporalio/strands-agents';
const APPROVED = new Set<string>();
export async function deleteThing(input: { name: string }): Promise<string> {
if (!APPROVED.has(input.name)) {
// First attempt: mark the name as approved on the way out (simulating the
// human flipping a flag during the interrupt pause) and stop the agent by
// raising an interrupt-shaped failure. The plugin's `StrandsFailureConverter`
// would also recognize a thrown `{ interrupts: [{ toJSON: () => ... }] }`,
// but throwing `ApplicationFailure` directly avoids any chance of the
// converter being skipped (and keeps `nonRetryable: true` so the workflow
// sees the interrupt instead of a retry-then-success).
APPROVED.add(input.name);
throw ApplicationFailure.create({
message: 'interrupt:approval',
type: STRANDS_INTERRUPT_TYPE,
nonRetryable: true,
details: [
{
id: `delete:${input.name}`,
name: 'approval',
reason: `approve delete of protected resource '${input.name}'?`,
source: 'tool',
},
],
});
}
return `deleted ${input.name}`;
}
The Workflow resumes the agent the same way as for a hook interrupt:
strands-agents/src/workflows/activity-interrupt.ts
import type { InterruptResponseContent, InterruptResponseContentData } from '@strands-agents/sdk';
import { TemporalAgent, workflow as strandsWorkflow } from '@temporalio/strands-agents';
import { condition, defineQuery, defineSignal, setHandler } from '@temporalio/workflow';
export const activityInterruptApproveSignal = defineSignal<[string]>('activityInterruptApprove');
export const activityInterruptPendingApprovalQuery = defineQuery<string | null>('activityInterruptPendingApproval');
export async function activityInterrupt(prompt: string): Promise<string> {
let approval: string | null = null;
let pendingReason: string | null = null;
setHandler(activityInterruptApproveSignal, (response) => {
approval = response;
});
setHandler(activityInterruptPendingApprovalQuery, () => pendingReason);
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
tools: [
strandsWorkflow.activityAsTool('deleteThing', {
description: 'Delete a thing by name.',
inputSchema: {
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
},
activityOptions: { startToCloseTimeout: '30 seconds', retry: { maximumAttempts: 3 } },
}),
],
});
let result = await agent.invoke(prompt);
while (result.stopReason === 'interrupt') {
const interrupts = result.interrupts ?? [];
pendingReason = (interrupts[0]?.reason as string | undefined) ?? null;
await condition(() => approval !== null);
const response = approval!;
approval = null;
pendingReason = null;
const responses: InterruptResponseContentData[] = interrupts.map((i) => ({
type: 'interruptResponse',
interruptResponse: { interruptId: i.id, response },
}));
result = await agent.invoke(responses as InterruptResponseContent[]);
}
return result.toString();
}
Activity-tool interrupts rely on the plugin's failure converter, which is installed via the client's data converter.
Attach StrandsPlugin to the client (not just the Worker) for Activity-tool interrupts to work. Workers built from
that client pick up the plugin automatically.
const client = new Client({ connection, plugins: [new StrandsPlugin({ models })] });
Return structured data from an agent
To have the agent return a typed object instead of free-form text, pass a structuredOutputSchema (any Zod schema) to
TemporalAgent. The values flow through the model Activity unchanged, and the parsed object is available on
result.structuredOutput:
strands-agents/src/workflows/structured-output.ts
import { TemporalAgent } from '@temporalio/strands-agents';
import { z } from 'zod';
export const PersonInfo = z.object({
name: z.string().describe('Name of the person'),
age: z.number().describe('Age of the person'),
occupation: z.string().describe('Occupation of the person'),
});
export type PersonInfo = z.infer<typeof PersonInfo>;
export async function structuredOutputWorkflow(prompt: string): Promise<PersonInfo> {
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
structuredOutputSchema: PersonInfo,
});
const result = await agent.invoke(prompt);
return result.structuredOutput as PersonInfo;
}
Stream agent output to clients
For long-running agent calls, you may want to forward model output chunks to an external consumer as they arrive rather than waiting for the full response.
Pass streamingTopic: '...' to TemporalAgent and host a WorkflowStream on the Workflow via
@temporalio/workflow-streams. Each
model stream event is published on the named topic from inside the model Activity. Subscribers read events through
WorkflowStreamClient. Chunks are batched on streamingBatchInterval (default '100 milliseconds').
Define the Workflow with a WorkflowStream and a streaming topic:
strands-agents/src/workflows/streaming.ts
import { TemporalAgent } from '@temporalio/strands-agents';
import { WorkflowStream } from '@temporalio/workflow-streams/workflow';
export async function streamingWorkflow(prompt: string): Promise<string> {
// Constructing the stream installs the publish/poll handlers that
// WorkflowStreamClient calls. Nothing in the workflow body reads from it.
void new WorkflowStream();
const agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
streamingTopic: 'events',
});
const result = await agent.invoke(prompt);
return result.toString();
}
Subscribe to the stream from a client:
strands-agents/src/streaming.ts
import { Client, Connection } from '@temporalio/client';
import { WorkflowStreamClient } from '@temporalio/workflow-streams/client';
import { streamingWorkflow } from './workflows';
interface StreamEvent {
type?: string;
delta?: { type?: string; text?: string };
}
async function run() {
const connection = await Connection.connect({
address: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233',
});
const client = new Client({ connection });
const workflowId = 'strands-streaming';
const handle = await client.workflow.start(streamingWorkflow, {
args: ['Count from 1 to 5, one number per sentence.'],
taskQueue: 'strands-agents',
workflowId,
});
const stream = WorkflowStreamClient.create(client, workflowId);
const consume = (async () => {
for await (const item of stream.subscribe<StreamEvent>(['events'], 0, {
pollCooldown: '50 milliseconds',
resultType: true,
})) {
const event = item.data;
if (event.type === 'modelContentBlockDeltaEvent' && event.delta?.type === 'textDelta' && event.delta.text) {
process.stdout.write(event.delta.text);
} else if (event.type === 'modelMessageStopEvent') {
process.stdout.write('\n');
return;
}
}
})();
const result = await handle.result();
await consume;
console.log(`Final result: ${result}`);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
Run in production
Configure retry policies, handle long-running chat sessions, and add distributed tracing.
Configure retries
TemporalAgent disables Strands' built-in ModelRetryStrategy so that retries are handled exclusively by Temporal.
Configure retries with activityOptions.retry on TemporalAgent for model calls, and on the Activity options accepted
by workflow.activityAsTool, workflow.activityAsHook, and TemporalMCPClient for their respective calls:
new TemporalAgent({
activityOptions: {
startToCloseTimeout: '60 seconds',
retry: { maximumAttempts: 3 },
},
});
Passing retryStrategy to new TemporalAgent(...) throws. Remove the argument (or pass retryStrategy: null) and use
activityOptions.retry instead.
Handle long-running chat sessions
A chat-style Workflow accumulates message history with every turn. Over a long session, the Workflow's event history can grow large enough to hit Temporal's per-Workflow history limit. To avoid this, use Continue-as-New to start a fresh Workflow execution while carrying the agent's message history forward as input.
In this example, each user turn arrives as a Workflow Update, so the
caller gets the agent's reply back from the same call. workflowInfo().continueAsNewSuggested flips to true once the
server decides history has grown large enough; the Workflow checks it after each turn and hands off to a fresh run,
carrying agent.messages as input:
strands-agents/src/workflows/continue-as-new.ts
import type { Message } from '@strands-agents/sdk';
import { TemporalAgent } from '@temporalio/strands-agents';
import {
allHandlersFinished,
condition,
continueAsNew,
defineQuery,
defineSignal,
defineUpdate,
setHandler,
workflowInfo,
} from '@temporalio/workflow';
export interface ChatInput {
messages?: Message[];
}
export const chatTurn = defineUpdate<string, [string]>('turn');
export const chatEnd = defineSignal('endChat');
export const chatMessages = defineQuery<Message[]>('messages');
export async function chatWorkflow(input: ChatInput = {}): Promise<void> {
let done = false;
let agent: TemporalAgent | null = null;
// Serialize concurrent `turn` updates so they can't interleave on `agent.messages`.
let pending: Promise<unknown> = Promise.resolve();
setHandler(chatTurn, async (prompt) => {
await condition(() => agent !== null);
const prev = pending;
let release!: () => void;
pending = new Promise<void>((resolve) => {
release = resolve;
});
try {
await prev;
const result = await agent!.invoke(prompt);
return result.toString().trim();
} finally {
release();
}
});
setHandler(chatEnd, () => {
done = true;
});
setHandler(chatMessages, () => (agent ? [...agent.messages] : []));
agent = new TemporalAgent({
activityOptions: { startToCloseTimeout: '60 seconds', retry: { maximumAttempts: 3 } },
messages: input.messages ?? [],
});
await condition(() => done || workflowInfo().continueAsNewSuggested);
// Drain in-flight `turn` updates before exiting or handing off.
await condition(allHandlersFinished);
if (!done) {
await continueAsNew<typeof chatWorkflow>({ messages: agent.messages });
}
}
Add tracing with OpenTelemetry
To get distributed traces across model, tool, and MCP Activities, combine StrandsPlugin with the
OpenTelemetry plugin.
Register OpenTelemetryPlugin on both the client and the Worker. You get OpenTelemetry spans around the model, tool,
and MCP Activities the plugin schedules, plus any spans Strands itself emits inside invoke:
import { OpenTelemetryPlugin } from '@temporalio/interceptors-opentelemetry';
import { Resource } from '@opentelemetry/resources';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { StrandsPlugin } from '@temporalio/strands-agents';
const otel = new OpenTelemetryPlugin({
resource: new Resource({ 'service.name': 'strands-worker' }),
spanProcessor: new SimpleSpanProcessor(new OTLPTraceExporter()),
});
// client
const client = new Client({ connection, plugins: [otel] });
// worker
const worker = await Worker.create({
connection,
taskQueue: 'strands-agents',
workflowsPath: require.resolve('./workflows'),
plugins: [otel, new StrandsPlugin({ models })],
});
Snapshots are not supported
TemporalAgent.takeSnapshot() and TemporalAgent.loadSnapshot() throw. Temporal's event history already persists
Workflow state durably at a finer granularity than Strands snapshots, so snapshots are redundant inside a Workflow.
Samples
The Strands Agents plugin samples demonstrate all supported patterns end-to-end.