From cba0175cf05602843357c0f3d2a1b87df7fe6c7b Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 2 Jun 2026 13:54:16 -0700 Subject: [PATCH 1/2] 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/2] 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