# Strands Agents integration

> Run Strands Agents AI workflows with durable execution using the Temporal TypeScript SDK and Strands plugin.

Temporal's integration with [Strands Agents](https://strandsagents.com/) is an [SDK Plugin](/develop/plugins-guide) that
gives your Strands agents [Durable Execution](/temporal#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.

> **ℹ️ Info:**
>
> 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](https://github.com/temporalio/samples-typescript/tree/main/strands-agents). 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](https://strandsagents.com/) for more details.
- If you are new to Temporal, read [Understanding Temporal](/evaluate/understanding-temporal) or take the
  [Temporal 101](https://learn.temporal.io/courses/temporal_101/) course.
- Set up your local development environment by following the
  [Set up your local with the TypeScript SDK](/develop/typescript/set-up-your-local-typescript) 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:

```bash
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:

<!--SNIPSTART typescript-strands-hello-world-workflow -->
[strands-agents/src/workflows/hello-world.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/hello-world.ts)
```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();
}
```
<!--SNIPEND-->

**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](#connect-to-mcp-servers):

<!--SNIPSTART typescript-strands-worker -->
[strands-agents/src/worker.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/worker.ts)
```ts
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);
});
```
<!--SNIPEND-->

**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:

<!--SNIPSTART typescript-strands-hello-world-client -->
[strands-agents/src/hello-world.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/hello-world.ts)
```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);
});
```
<!--SNIPEND-->

## 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:

```ts
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:

<!--SNIPSTART typescript-strands-tools-activity -->
[strands-agents/src/activities/tools.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/activities/tools.ts)
```ts
export async function fetchWeather(input: { city: string }): Promise<{ city: string; temperatureF: number; conditions: string }> {
  return {
    city: input.city,
    temperatureF: 72,
    conditions: 'sunny',
  };
}
```
<!--SNIPEND-->

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:

<!--SNIPSTART typescript-strands-tools-workflow -->
[strands-agents/src/workflows/tools.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/tools.ts)
```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();
}
```
<!--SNIPEND-->

Register the Activity functions on the Worker by passing them in the `activities` option, as shown in
[Start a Worker](#run-a-strands-agent-with-durable-execution). The `activityName` passed to `activityAsTool` must match
the name the Activity is registered under.

### React to agent lifecycle events

Strands' [hook system](https://strandsagents.com/) 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):

<!--SNIPSTART typescript-strands-hooks-workflow -->
[strands-agents/src/workflows/hooks.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/hooks.ts)
```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;
}
```
<!--SNIPEND-->

The Activity dispatched by the hook is a normal Temporal Activity registered on the Worker:

<!--SNIPSTART typescript-strands-hooks-activity -->
[strands-agents/src/activities/hooks.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/activities/hooks.ts)
```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`);
}
```
<!--SNIPEND-->

> **⚠️ Caution:**
>
> Hook callbacks run in Workflow context, so they must be
> [deterministic](/develop/typescript/workflows/basics#workflow-logic-requirements). 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](https://modelcontextprotocol.io/) 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`:

<!--SNIPSTART typescript-strands-mcp-workflow -->
[strands-agents/src/workflows/mcp.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/mcp.ts)
```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();
}
```
<!--SNIPEND-->

Register the MCP client factory on the Worker via `mcpClients`, as shown in the
[Worker](#run-a-strands-agent-with-durable-execution) 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`:

```ts
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`:

```ts
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:

<!--SNIPSTART typescript-strands-human-in-the-loop-workflow -->
[strands-agents/src/workflows/human-in-the-loop.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/human-in-the-loop.ts)
```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();
}
```
<!--SNIPEND-->

#### 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:

<!--SNIPSTART typescript-strands-activity-interrupt-activity -->
[strands-agents/src/activities/activity-interrupt.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/activities/activity-interrupt.ts)
```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}`;
}
```
<!--SNIPEND-->

The Workflow resumes the agent the same way as for a hook interrupt:

<!--SNIPSTART typescript-strands-activity-interrupt-workflow -->
[strands-agents/src/workflows/activity-interrupt.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/activity-interrupt.ts)
```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();
}
```
<!--SNIPEND-->

> **⚠️ Caution:**
>
> 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.
>
> ```ts
> 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`:

<!--SNIPSTART typescript-strands-structured-output-workflow -->
[strands-agents/src/workflows/structured-output.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/structured-output.ts)
```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;
}
```
<!--SNIPEND-->

### 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`](https://github.com/temporalio/sdk-typescript/tree/main/packages/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:

<!--SNIPSTART typescript-strands-streaming-workflow -->
[strands-agents/src/workflows/streaming.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/streaming.ts)
```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();
}
```
<!--SNIPEND-->

Subscribe to the stream from a client:

<!--SNIPSTART typescript-strands-streaming-client -->
[strands-agents/src/streaming.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/streaming.ts)
```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);
});
```
<!--SNIPEND-->

## 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:

```ts
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](/develop/typescript/workflows/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](/develop/typescript/workflows/message-passing#updates), 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:

<!--SNIPSTART typescript-strands-continue-as-new-workflow -->
[strands-agents/src/workflows/continue-as-new.ts](https://github.com/temporalio/samples-typescript/blob/main/strands-agents/src/workflows/continue-as-new.ts)
```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 });
  }
}
```
<!--SNIPEND-->

### Add tracing with OpenTelemetry

To get distributed traces across model, tool, and MCP Activities, combine `StrandsPlugin` with the
[OpenTelemetry plugin](https://github.com/temporalio/sdk-typescript/tree/main/packages/interceptors-opentelemetry).
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`:

```ts
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](https://github.com/temporalio/samples-typescript/tree/main/strands-agents)
demonstrate all supported patterns end-to-end.
