From cba0175cf05602843357c0f3d2a1b87df7fe6c7b Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 2 Jun 2026 13:54:16 -0700 Subject: [PATCH 1/4] Add Strands Agents integration guide for TypeScript Adds a TypeScript Strands Agents integration guide at docs/develop/typescript/integrations/strands-agents.mdx, mirroring the Python guide. Links it from the integrations index, the sidebar, and the /develop/typescript landing page. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/develop/typescript/index.mdx | 1 + .../develop/typescript/integrations/index.mdx | 1 + .../integrations/strands-agents.mdx | 881 ++++++++++++++++++ sidebars.js | 2 +- 4 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 docs/develop/typescript/integrations/strands-agents.mdx diff --git a/docs/develop/typescript/index.mdx b/docs/develop/typescript/index.mdx index d56f191890..741a1d4bcb 100644 --- a/docs/develop/typescript/index.mdx +++ b/docs/develop/typescript/index.mdx @@ -83,6 +83,7 @@ Once your local Temporal Service is set up, continue building with the following Integrate the Vercel AI SDK with Temporal to build durable AI agents and AI-powered applications. - [Vercel AI SDK Integration](/develop/typescript/integrations/ai-sdk) +- [Strands Agents integration](/develop/typescript/integrations/strands-agents) ## Temporal TypeScript Technical Resources diff --git a/docs/develop/typescript/integrations/index.mdx b/docs/develop/typescript/integrations/index.mdx index 371291145a..f3029a899b 100644 --- a/docs/develop/typescript/integrations/index.mdx +++ b/docs/develop/typescript/integrations/index.mdx @@ -21,3 +21,4 @@ The following AI framework and tooling integrations are available for the Tempor | AI SDK by Vercel | Agent framework | [ai-sdk.dev](https://ai-sdk.dev/docs/introduction) | [Guide](/develop/typescript/integrations/ai-sdk) | | Braintrust | Observability | [braintrust.dev](https://braintrust.dev/docs) | [Guide](https://www.braintrust.dev/docs/integrations/sdk-integrations/temporal#typescript) | | Mastra | Agent framework | [mastra.ai](https://mastra.ai/docs) | [Guide](https://mastra.ai/guides/deployment/temporal) | +| Strands Agents | Agent framework | [strandsagents.com](https://strandsagents.com/) | [Guide](/develop/typescript/integrations/strands-agents) | diff --git a/docs/develop/typescript/integrations/strands-agents.mdx b/docs/develop/typescript/integrations/strands-agents.mdx new file mode 100644 index 0000000000..f5cb69789b --- /dev/null +++ b/docs/develop/typescript/integrations/strands-agents.mdx @@ -0,0 +1,881 @@ +--- +id: strands-agents +title: Strands Agents integration +sidebar_label: Strands Agents +toc_max_heading_level: 3 +keywords: + - ai + - agents + - strands + - strands agents + - durable execution + - ai workflows +tags: + - Strands Agents + - TypeScript SDK + - Temporal SDKs +description: 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: + + +[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 { + 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](#connect-to-mcp-servers): + + +[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); +}); +``` + + +**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](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); +}); +``` + + +## 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 { + 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](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', + }; +} +``` + + +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](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 { + 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](#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): + + +[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 { + 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](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 { + // In production, write to a database / S3 / your audit pipeline. + log.info(`audit: tool ${toolName} completed`); +} +``` + + +:::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`: + + +[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 { + 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](#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: + + +[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('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 { + 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({ + 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](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(); + +export async function deleteThing(input: { name: string }): Promise { + 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](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('activityInterruptPendingApproval'); + +export async function activityInterrupt(prompt: string): Promise { + 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(); +} +``` + + +:::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`: + + +[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; + +export async function structuredOutputWorkflow(prompt: string): Promise { + 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`](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: + + +[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 { + // 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](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(['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: + +```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: + + +[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('turn'); +export const chatEnd = defineSignal('endChat'); +export const chatMessages = defineQuery('messages'); + +export async function chatWorkflow(input: ChatInput = {}): Promise { + let done = false; + let agent: TemporalAgent | null = null; + // Serialize concurrent `turn` updates so they can't interleave on `agent.messages`. + let pending: Promise = Promise.resolve(); + + setHandler(chatTurn, async (prompt) => { + await condition(() => agent !== null); + const prev = pending; + let release!: () => void; + pending = new Promise((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({ messages: agent.messages }); + } +} +``` + + +### 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. diff --git a/sidebars.js b/sidebars.js index 79d3fbd669..c3ef649748 100644 --- a/sidebars.js +++ b/sidebars.js @@ -760,7 +760,7 @@ module.exports = { type: 'doc', id: 'develop/typescript/integrations/index', }, - items: ['develop/typescript/integrations/ai-sdk'], + items: ['develop/typescript/integrations/ai-sdk', 'develop/typescript/integrations/strands-agents'], }, ], }, From 12babf47d346ca82e81aab7ce7766ee43d269da2 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 2 Jun 2026 13:57:06 -0700 Subject: [PATCH 2/4] Reference all TypeScript integrations from the landing page Mirrors the Python landing page: replaces the Vercel-only section with a single Integrations section linking to all four TypeScript integrations (Braintrust, Mastra, Strands Agents, Vercel AI SDK). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/develop/typescript/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/develop/typescript/index.mdx b/docs/develop/typescript/index.mdx index 741a1d4bcb..c02c48c7c4 100644 --- a/docs/develop/typescript/index.mdx +++ b/docs/develop/typescript/index.mdx @@ -78,12 +78,12 @@ Once your local Temporal Service is set up, continue building with the following - [Converters and encryption](/develop/typescript/converters-and-encryption) - [Entity pattern](/develop/typescript/best-practices/entity-pattern) -## [Vercel AI SDK Integration](/develop/typescript/integrations/ai-sdk) +## [Integrations](/develop/typescript/integrations) -Integrate the Vercel AI SDK with Temporal to build durable AI agents and AI-powered applications. - -- [Vercel AI SDK Integration](/develop/typescript/integrations/ai-sdk) +- [Braintrust integration](https://www.braintrust.dev/docs/integrations/sdk-integrations/temporal#typescript) +- [Mastra integration](https://mastra.ai/guides/deployment/temporal) - [Strands Agents integration](/develop/typescript/integrations/strands-agents) +- [Vercel AI SDK integration](/develop/typescript/integrations/ai-sdk) ## Temporal TypeScript Technical Resources From acf07c9114ad2192fc8ccef5437f6f09889ca219 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Thu, 2 Jul 2026 15:19:47 -0700 Subject: [PATCH 3/4] Address Vale term and heading warnings on Strands Agents TS guide Fixes casing on Temporal terms (Durable Execution, Retry Policy, Task Queue, Timer, Activity, Failure Converter, Data Converter, Event History) and the technical-resources heading. Adds MCP and OpenTelemetry as Headings exceptions since they were valid proper nouns mid-heading, not miscapitalized text. Drops 'audit logging' from the required-term substitution list. --- docs/develop/typescript/index.mdx | 2 +- .../typescript/integrations/strands-agents.mdx | 18 +++++++++--------- vale/styles/Temporal/Headings.yml | 2 ++ vale/styles/Temporal/terms.yml | 1 - 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/develop/typescript/index.mdx b/docs/develop/typescript/index.mdx index 6e8f426c1a..d914cb9da4 100644 --- a/docs/develop/typescript/index.mdx +++ b/docs/develop/typescript/index.mdx @@ -87,7 +87,7 @@ Once your local Temporal Service is set up, continue building with the following - [Vercel AI SDK integration](/develop/typescript/integrations/ai-sdk) - [OpenAI Agents SDK Integration](/develop/typescript/integrations/openai-agents) -## Temporal TypeScript Technical Resources +## Temporal TypeScript technical resources - [TypeScript SDK Quickstart - Setup Guide](/develop/typescript/set-up-your-local-typescript) - [TypeScript API Documentation](https://typescript.temporal.io) diff --git a/docs/develop/typescript/integrations/strands-agents.mdx b/docs/develop/typescript/integrations/strands-agents.mdx index f5cb69789b..1a5dd63770 100644 --- a/docs/develop/typescript/integrations/strands-agents.mdx +++ b/docs/develop/typescript/integrations/strands-agents.mdx @@ -14,7 +14,7 @@ tags: - Strands Agents - TypeScript SDK - Temporal SDKs -description: Run Strands Agents AI workflows with durable execution using the Temporal TypeScript SDK and Strands plugin. +description: 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 @@ -58,7 +58,7 @@ 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, +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** @@ -211,7 +211,7 @@ new StrandsPlugin({ }); ``` -Each `TemporalAgent` carries its own Activity options (timeouts, retry policy, task queue, streaming topic) and +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. @@ -412,7 +412,7 @@ const echo = new TemporalMCPClient({ 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 +(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 @@ -508,10 +508,10 @@ export async function humanInTheLoop(prompt: string): Promise { ``` -#### Interrupt from an activity tool +#### 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 +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: @@ -610,7 +610,7 @@ export async function activityInterrupt(prompt: string): Promise { :::caution -Activity-tool interrupts rely on the plugin's failure converter, which is installed via the client's data converter. +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. @@ -763,7 +763,7 @@ Passing `retryStrategy` to `new TemporalAgent(...)` throws. Remove the argument ### 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 +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. @@ -872,7 +872,7 @@ const worker = await Worker.create({ ### Snapshots are not supported -`TemporalAgent.takeSnapshot()` and `TemporalAgent.loadSnapshot()` throw. Temporal's event history already persists +`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 diff --git a/vale/styles/Temporal/Headings.yml b/vale/styles/Temporal/Headings.yml index 079dedafee..c92f810dbc 100644 --- a/vale/styles/Temporal/Headings.yml +++ b/vale/styles/Temporal/Headings.yml @@ -19,7 +19,9 @@ exceptions: - Linux - macOS - Marketplace + - MCP - MongoDB + - OpenTelemetry - REPL - Studio - TypeScript diff --git a/vale/styles/Temporal/terms.yml b/vale/styles/Temporal/terms.yml index ecf3921314..4b9e17a34d 100644 --- a/vale/styles/Temporal/terms.yml +++ b/vale/styles/Temporal/terms.yml @@ -18,7 +18,6 @@ swap: advanced visibility: Advanced Visibility archival: Archival asynchronous activity completion: Asynchronous Activity Completion - audit logging: Audit Logging child workflow: Child Workflow child workflow execution: Child Workflow Execution claim mapper: Claim Mapper From 8ae5fb3835c6d60574d496e90ffc9cdced6ca16a Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Thu, 2 Jul 2026 15:21:58 -0700 Subject: [PATCH 4/4] Fix missing comma in sidebars.js breaking the build The strands-agents sidebar entry was missing a trailing comma, causing a ParseError when Docusaurus loaded sidebars.js. --- sidebars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sidebars.js b/sidebars.js index f98b9608c2..491df75aa9 100644 --- a/sidebars.js +++ b/sidebars.js @@ -722,7 +722,7 @@ module.exports = { }, items: [ 'develop/typescript/integrations/ai-sdk', - 'develop/typescript/integrations/strands-agents' + 'develop/typescript/integrations/strands-agents', 'develop/typescript/integrations/openai-agents', ], },