workflow 5.0.0-beta.50 → 5.0.0-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Serializable Steps
3
- description: Wrap non-serializable third-party objects (like AI model providers) inside step factory functions so they can cross the workflow boundary.
3
+ description: Wrap non-serializable third-party objects, including AI provider models and cloud clients, inside step factory functions.
4
4
  type: guide
5
- summary: Return a callback from a step to defer construction of a non-owned class (AI SDK models, cloud SDK clients) until execution time, making them usable inside durable workflows.
5
+ summary: Defer construction of non-owned AI provider models and cloud SDK clients until step execution so they remain usable in durable workflows.
6
6
  related:
7
7
  - /docs/foundations/serialization
8
8
  - /docs/foundations/serialization#custom-class-serialization
@@ -10,7 +10,7 @@ related:
10
10
  ---
11
11
 
12
12
  <CopyPrompt
13
- text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing the object (AI SDK model, cloud SDK client) into the workflow, export a factory that returns an async callback marked with &quot;use step&quot; which constructs and returns the object at execution time, for example `export function openai(...args) { return async () => { &quot;use step&quot;; return openaiProvider(...args); }; }`. Pass the factory across the workflow boundary (the compiler serializes the function reference, not the instance) and invoke it inside steps where full Node.js access is available. Keep the factory's constructor arguments serializable. Verify the workflow builds, replays deterministically, and the dependency is only instantiated during step execution."
13
+ text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing an AI provider model, cloud SDK client, or other class instance into the workflow, export a factory that returns an async callback marked with &quot;use step&quot;. Capture only serializable constructor options, construct the provider or client inside the step, and keep the instance inside that step's execution. Verify the workflow builds, replays deterministically, and never serializes the live dependency."
14
14
  />
15
15
 
16
16
  <Callout>
@@ -22,97 +22,68 @@ This is an advanced guide. It dives into workflow internals and is not required
22
22
  Workflow functions run inside a sandboxed VM where every value that crosses a function boundary must be serializable. There are two ways to get a non-serializable object across that boundary, depending on whether you own the class:
23
23
 
24
24
  - **You own the class**: implement the [`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE` protocol](/docs/foundations/serialization#custom-class-serialization). The instance becomes a first-class serializable value: you can pass it as a workflow input, return it from a step, and call `"use step"` instance methods on it directly. This is the right tool when the class is yours to modify.
25
- - **You don't own the class**: you can't add methods to `openai("gpt-4o")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction in a `"use step"` factory function and pass the factory across the boundary. That's what this page covers.
25
+ - **You don't own the class**: you can't add serialization methods to `openai("gpt-5.6-sol")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction and use in a `"use step"` factory function. That's what this page covers.
26
26
 
27
27
  ## The problem
28
28
 
29
- AI SDK model providers (`openai("gpt-4o")`, `anthropic("claude-sonnet-4-20250514")`, etc.) return complex objects with methods, closures, and internal state. Passing one directly into a step causes a serialization error, and you can't bolt `WORKFLOW_SERIALIZE` onto a third-party class.
29
+ AI SDK provider models and cloud SDK clients often contain methods, closures, sockets, and internal state. Passing one across a workflow boundary causes a serialization error, and you can't add `WORKFLOW_SERIALIZE` to a class you don't own.
30
30
 
31
31
  ```typescript lineNumbers
32
- import { openai } from "@ai-sdk/openai";
33
- import { DurableAgent } from "@workflow/ai/agent";
34
- import { getWritable } from "workflow";
35
- import type { UIMessageChunk } from "ai";
36
-
37
- export async function brokenAgent(prompt: string) {
38
- "use workflow";
32
+ import { S3Client } from "@aws-sdk/client-s3";
39
33
 
40
- const writable = getWritable<UIMessageChunk>();
41
- const agent = new DurableAgent({
42
- // This fails: the model object is not serializable
43
- model: openai("gpt-4o"),
44
- });
45
-
46
- await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
34
+ async function uploadFile(client: S3Client, key: string) {
35
+ "use step";
36
+ // ... upload with client ...
47
37
  }
48
- ```
49
38
 
50
- ## The solution: step-as-factory
51
-
52
- Instead of passing the model object, pass a **callback function** that returns the model. Marking that callback with `"use step"` tells the compiler to serialize the *function reference* (which is a string identifier) rather than its return value. The provider is only instantiated at execution time, inside the step's full Node.js runtime.
53
-
54
- ```typescript lineNumbers
55
- import { openai as openaiProvider } from "@ai-sdk/openai";
39
+ export async function brokenUpload(region: string, key: string) {
40
+ "use workflow";
56
41
 
57
- // Returns a step function, not a model object
58
- export function openai(...args: Parameters<typeof openaiProvider>) {
59
- return async () => {
60
- "use step";
61
- return openaiProvider(...args); // [!code highlight]
62
- };
42
+ const client = new S3Client({ region });
43
+ await uploadFile(client, key); // Fails: S3Client is not serializable
63
44
  }
64
45
  ```
65
46
 
66
- The `DurableAgent` receives a function (`() => Promise<LanguageModel>`) instead of a model object. When the agent needs to call the large language model (LLM), it invokes the factory inside a step where the real provider can be constructed with full Node.js access.
47
+ ## The solution: step-as-factory
67
48
 
68
- ## How `@workflow/ai` uses this
49
+ Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
69
50
 
70
- <Callout type="warn">
71
- `@workflow/ai`'s pre-wrapped providers and `DurableAgent` are deprecated. AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) resolves models from AI Gateway model strings (for example, `"openai/gpt-4o"`), which usually removes the need for a model factory; see the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The serialization pattern on this page still applies to any non-serializable dependency you own (for example, cloud SDK clients).
51
+ <Callout type="info">
52
+ A plain Vercel AI Gateway model string such as `"spacexai/grok-4.6"` is already serializable and does not need a factory.
72
53
  </Callout>
73
54
 
74
- The `@workflow/ai` package ships pre-wrapped providers for all major AI SDK backends. Each one follows the same pattern:
55
+ ### AI provider example
56
+
57
+ When using an AI SDK provider package, construct and use its model inside the step. The outer factory captures only the serializable model ID:
75
58
 
76
59
  ```typescript lineNumbers
77
- // packages/ai/src/providers/anthropic.ts
78
- import { anthropic as anthropicProvider } from "@ai-sdk/anthropic";
60
+ import { openai } from "@ai-sdk/openai";
61
+ import { generateText } from "ai";
79
62
 
80
- export function anthropic(...args: Parameters<typeof anthropicProvider>) {
81
- return async () => {
63
+ export function createOpenAIGenerator(modelId: string) {
64
+ return async (prompt: string) => {
82
65
  "use step";
83
- return anthropicProvider(...args); // [!code highlight]
66
+ const { text } = await generateText({ model: openai(modelId), prompt });
67
+ return text;
84
68
  };
85
69
  }
86
- ```
87
70
 
88
- This means you import from `@workflow/ai` instead of `@ai-sdk/*` directly:
89
-
90
- ```typescript lineNumbers
91
- import { anthropic } from "@workflow/ai/anthropic";
92
- import { DurableAgent } from "@workflow/ai/agent";
93
- import { getWritable } from "workflow";
94
- import type { UIMessageChunk } from "ai";
95
-
96
- export async function chatAgent(prompt: string) {
71
+ export async function summarize(prompt: string) {
97
72
  "use workflow";
98
73
 
99
- const writable = getWritable<UIMessageChunk>();
100
- const agent = new DurableAgent({
101
- model: anthropic("claude-sonnet-4-20250514"), // [!code highlight]
102
- });
103
-
104
- await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
74
+ const generate = createOpenAIGenerator("gpt-5.6-sol");
75
+ return generate(prompt);
105
76
  }
106
77
  ```
107
78
 
108
- ## Writing your own serializable wrapper
79
+ The same structure works with provider packages such as `@ai-sdk/anthropic` and `@ai-sdk/google`: capture serializable configuration in the outer function and keep the provider object inside the step. For durable agent loops, continue to use `WorkflowAgent`; the factory pattern is for lower-level AI SDK calls and other third-party dependencies.
109
80
 
110
- Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
81
+ ### Cloud client example
111
82
 
112
83
  ```typescript lineNumbers
113
84
  import type { S3Client as S3ClientType } from "@aws-sdk/client-s3";
114
85
 
115
- // The arguments (region, bucket) are plain strings, which are serializable
86
+ // The region is a plain string, which is serializable
116
87
  export function createS3Client(region: string) {
117
88
  return async (): Promise<S3ClientType> => {
118
89
  "use step";
@@ -152,4 +123,5 @@ async function uploadFile(
152
123
  - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): Marks a function for extraction and serialization.
153
124
  - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions): Declares the orchestrator function.
154
125
  - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Provides AI SDK's durable agent, resolves models through AI Gateway strings, and replaces `DurableAgent`.
126
+ - [AI SDK providers](https://ai-sdk.dev/providers/ai-sdk-providers): Lists direct provider packages and configuration.
155
127
  - [Custom class serialization](/docs/foundations/serialization#custom-class-serialization): Provides the companion pattern for classes you own (`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`).
@@ -1,54 +1,59 @@
1
1
  ---
2
2
  title: Agent Cancellation
3
- description: Cancel a running agent from the outside using AbortSignal. A hook fires the abort, the agent step bails out of the model stream, and the client gets a clean stop notification.
3
+ description: Cancel a running WorkflowAgent from the outside using AbortSignal and a durable stop hook.
4
4
  type: guide
5
- summary: Cancel a running agent cooperatively with AbortController. A stop hook fires controller.abort(), the signal propagates into the agent step to cancel the model stream, and a data-stopped part is emitted to streaming clients before the workflow returns.
5
+ summary: Cancel a running WorkflowAgent cooperatively with AbortController so the model stream stops and the workflow can return a clean status.
6
6
  ---
7
7
 
8
8
  <CopyPrompt
9
- text="Add cancellation to this durable AI agent. For hard cancellation, expose a server route that receives `runId` and calls `getRun(runId).cancel()` from `workflow/api`. For graceful stop, define `stopHook` with `defineHook()` from `workflow`, create it with a stable token such as the workflow run ID, and race the agent loop against the stop hook using `Promise.race`. Use `getWritable<UIMessageChunk>()` to emit a final stopped/canceled message before returning. Wire the UI Stop button to the route that resumes the hook or falls back to `getRun(runId).cancel()`. Verify active model/tool work stops, cleanup runs for graceful stop, and stale run IDs are handled."
9
+ text="Add cancellation to this AI SDK WorkflowAgent. For hard cancellation, expose a server route that receives `runId` and calls `getRun(runId).cancel()` from `workflow/api`. For graceful cancellation, define `stopHook` with `defineHook()` from `workflow`, create it with a stable token such as the workflow run ID, and race `WorkflowAgent.stream()` against the stop hook using `Promise.race`. Pass an `AbortController` signal to the agent, forward the tool execution signal to cancellable I/O, and wait for the agent branch to settle after aborting before returning a stopped status. Wire the UI Stop button to the route that resumes the hook or falls back to `getRun(runId).cancel()`, and verify stale run IDs are handled."
10
10
  />
11
11
 
12
12
  Cancel a running agent from the outside through a **Stop** button in a chat user interface (UI), an admin cancellation endpoint, or a timeout fallback.
13
13
 
14
- <Callout type="warn">
15
- This recipe uses the deprecated `DurableAgent` API. For new agents, use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) and follow the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The cancellation patterns here (`run.cancel()`, stop-signal hook + `Promise.race`, `AbortController`) apply to either API.
16
- </Callout>
17
-
18
14
  ## Pattern
19
15
 
20
- Create an `AbortController` in the workflow and race the agent (passing its signal) against a stop hook. When the hook fires, `controller.abort()` is called: the signal propagates into the agent step and cancels the underlying model stream. Before returning, a `data-stopped` part is written to the stream so any streaming clients can render a clean end state.
16
+ Create an `AbortController` in the workflow and race the agent (passing its signal) against a stop hook. When the hook fires, call `controller.abort()`, then wait for the agent branch to observe the signal and settle before the workflow returns. The signal cancels the underlying model stream and is passed to tool execution; each tool must forward it to cancellable I/O. The workflow return value records whether it completed or stopped; read that value through the run API or persist it in application state.
21
17
 
18
+ {/* @skip-typecheck: requires AI SDK 7 and @ai-sdk/workflow */}
22
19
  ```typescript lineNumbers
23
- import { DurableAgent } from "@workflow/ai/agent";
20
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
21
+ import {
22
+ isStepCount,
23
+ tool,
24
+ type ModelMessage,
25
+ type ToolExecutionOptions,
26
+ } from "ai";
24
27
  import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
25
28
  import { z } from "zod";
26
- import type { ModelMessage, UIMessageChunk } from "ai";
27
29
 
28
30
  export const stopHook = defineHook({
29
31
  schema: z.object({ reason: z.string().optional() }),
30
32
  });
31
33
 
32
- async function searchWeb({ query }: { query: string }) {
33
- "use step";
34
- await new Promise((r) => setTimeout(r, 1500));
35
- return { results: [{ title: `${query} - Wikipedia`, snippet: `Overview of ${query}...` }] };
36
- }
37
-
38
- async function analyzeData({ topic }: { topic: string }) {
34
+ async function searchWeb(
35
+ { query }: { query: string },
36
+ { abortSignal }: ToolExecutionOptions<unknown>,
37
+ ) {
39
38
  "use step";
40
- await new Promise((r) => setTimeout(r, 1200));
41
- return { summary: `Analysis of ${topic}: significant developments found.`, confidence: 0.85 };
39
+ const response = await fetch(
40
+ `https://api.example.com/search?q=${encodeURIComponent(query)}`,
41
+ { signal: abortSignal }, // [!code highlight]
42
+ );
43
+ return response.json();
42
44
  }
43
45
 
44
- async function emitStopSignal(details: { reason?: string }) {
46
+ async function analyzeData(
47
+ { topic }: { topic: string },
48
+ { abortSignal }: ToolExecutionOptions<unknown>,
49
+ ) {
45
50
  "use step";
46
- const writer = getWritable<UIMessageChunk>().getWriter();
47
- try {
48
- await writer.write({ type: "data-stopped", id: "stop-signal", data: details } as UIMessageChunk);
49
- } finally {
50
- writer.releaseLock();
51
- }
51
+ const response = await fetch("https://api.example.com/analyze", {
52
+ method: "POST",
53
+ body: JSON.stringify({ topic }),
54
+ signal: abortSignal, // [!code highlight]
55
+ });
56
+ return response.json();
52
57
  }
53
58
 
54
59
  export async function stoppableAgent(messages: ModelMessage[]) {
@@ -58,43 +63,48 @@ export async function stoppableAgent(messages: ModelMessage[]) {
58
63
  const controller = new AbortController(); // [!code highlight]
59
64
  const hook = stopHook.create({ token: `stop:${workflowRunId}` });
60
65
 
61
- const agent = new DurableAgent({
62
- model: "anthropic/claude-haiku-4.5",
66
+ const agent = new WorkflowAgent({
67
+ model: "spacexai/grok-4.6",
63
68
  instructions: "You are a research assistant. Search and analyze data as needed.",
64
69
  tools: {
65
- searchWeb: {
70
+ searchWeb: tool({
66
71
  description: "Search the web for information",
67
72
  inputSchema: z.object({ query: z.string() }),
68
73
  execute: searchWeb,
69
- },
70
- analyzeData: {
74
+ }),
75
+ analyzeData: tool({
71
76
  description: "Analyze a piece of data",
72
77
  inputSchema: z.object({ topic: z.string() }),
73
78
  execute: analyzeData,
74
- },
79
+ }),
75
80
  },
76
81
  });
77
82
 
78
- const result = await Promise.race([
79
- agent
80
- .stream({
81
- messages,
82
- writable: getWritable<UIMessageChunk>(),
83
- abortSignal: controller.signal, // [!code highlight]
84
- maxSteps: 15,
85
- })
86
- .then((r) => ({ type: "complete" as const, messages: r.messages })),
87
- hook.then(({ reason }) => {
88
- controller.abort(reason); // [!code highlight]
89
- return { type: "stopped" as const, reason };
90
- }),
83
+ const agentPromise = agent.stream({
84
+ messages,
85
+ writable: getWritable<ModelCallStreamPart>(),
86
+ abortSignal: controller.signal, // [!code highlight]
87
+ stopWhen: isStepCount(15),
88
+ });
89
+
90
+ const outcome = await Promise.race([
91
+ agentPromise.then((result) => ({
92
+ type: "complete" as const,
93
+ messages: result.messages,
94
+ })),
95
+ hook.then(({ reason }) => ({
96
+ type: "stop-requested" as const,
97
+ reason,
98
+ })),
91
99
  ]);
92
100
 
93
- if (result.type === "stopped") {
94
- await emitStopSignal({ reason: result.reason });
101
+ if (outcome.type === "stop-requested") {
102
+ controller.abort(outcome.reason); // [!code highlight]
103
+ await agentPromise; // Wait until the losing branch has stopped. // [!code highlight]
104
+ return { type: "stopped" as const, reason: outcome.reason };
95
105
  }
96
106
 
97
- return result;
107
+ return outcome;
98
108
  }
99
109
  ```
100
110
 
@@ -145,20 +155,20 @@ export function StopButton({ runId }: { runId: string }) {
145
155
  1. The workflow creates an `AbortController` when it starts.
146
156
  2. The workflow creates a hook with the token `stop:${workflowRunId}`.
147
157
  3. `Promise.race` runs the agent stream and the stop hook concurrently.
148
- 4. The agent receives `controller.signal`. When aborted, the signal cancels the underlying model stream.
149
- 5. When the stop API resumes the hook, the workflow calls `controller.abort()`, resolves the race, and exits.
150
- 6. `emitStopSignal` writes a `data-stopped` part to the stream so the client renders a clean stop state.
158
+ 4. The agent receives `controller.signal`. When aborted, the signal cancels the active model stream, propagates to tool execution, and prevents another model step from starting. Tool implementations must forward it to operations such as `fetch` that support cancellation.
159
+ 5. When the stop API resumes the hook, the race reports a stop request. The workflow aborts the controller and awaits `agentPromise`, so it does not return while that branch is still running.
160
+ 6. After the agent branch settles, the workflow returns a `stopped` result. That return value is not written to the model-call stream automatically; the application can read `run.returnValue` or persist the status separately.
151
161
 
152
162
  ## Adapting this
153
163
 
154
164
  - **Add a timeout**: Race a third `sleep()` promise to stop automatically after a deadline.
155
165
  - **Audit logging**: Include a `reason` field in the stop schema to record who stopped the agent and why.
156
166
  - **Cross-process**: The hook token is deterministic, so any process can call `stopHook.resume()` with the run ID.
157
- - **Step limits**: Combine the pattern with `maxSteps` on the agent to cap execution without a manual stop.
167
+ - **Step limits**: Combine the pattern with `stopWhen: isStepCount(...)` to cap execution without a manual stop.
158
168
 
159
169
  ## Key APIs
160
170
 
161
171
  - [`defineHook()`](/docs/api-reference/workflow/define-hook): Defines a type-safe hook for the stop signal.
162
172
  - [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata): Provides the run ID for deterministic hook tokens.
163
- - [`getWritable()`](/docs/api-reference/workflow/get-writable): Streams output and the stop notification to the client.
164
- - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK's durable agent that respects the abort signal and replaces `DurableAgent`.
173
+ - [`getWritable()`](/docs/api-reference/workflow/get-writable): Stores durable model-call output from the agent.
174
+ - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK's durable agent that applies the abort signal to model calls and replaces `DurableAgent`.
@@ -11,12 +11,32 @@ summary: Build durable, resumable AI agents with AI SDK v7's WorkflowAgent.
11
11
 
12
12
  ## WorkflowAgent from AI SDK v7
13
13
 
14
- Use AI SDK v7's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for new durable agent work. It replaces `DurableAgent` and keeps the current agent pattern in the AI SDK package.
14
+ Use AI SDK v7's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for durable agents in Workflow 5. It replaces `DurableAgent` and keeps the current agent pattern in the AI SDK package.
15
15
 
16
16
  - Import `WorkflowAgent` from `@ai-sdk/workflow` and run it inside a `"use workflow"` function.
17
+ - Pass `"spacexai/grok-4.6"` as a plain model string so AI SDK routes requests through Vercel AI Gateway.
17
18
  - Stream `ModelCallStreamPart` chunks with `getWritable()`, then convert the run stream to UI message chunks with `createModelCallToUIChunkTransform()` in your route.
18
19
  - Mark tool `execute` functions with `"use step"` when they should run as durable workflow steps with retry and observability behavior.
19
20
 
21
+ ```typescript
22
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
23
+ import { getWritable } from "workflow";
24
+
25
+ export async function agentWorkflow(prompt: string) {
26
+ "use workflow";
27
+
28
+ const agent = new WorkflowAgent({
29
+ model: "spacexai/grok-4.6",
30
+ instructions: "You are a helpful assistant.",
31
+ });
32
+
33
+ return agent.stream({
34
+ messages: [{ role: "user", content: prompt }],
35
+ writable: getWritable<ModelCallStreamPart>(),
36
+ });
37
+ }
38
+ ```
39
+
20
40
  <Callout type="warn">
21
41
  `DurableAgent` is deprecated and remains documented for existing code only. See the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent), or the [`DurableAgent` API reference](/docs/api-reference/workflow-ai/durable-agent) while migrating.
22
42
  </Callout>