workflow 5.0.0-beta.2 → 5.0.0-beta.4

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.
Files changed (42) hide show
  1. package/dist/api-workflow.d.ts +1 -1
  2. package/dist/api-workflow.d.ts.map +1 -1
  3. package/dist/api-workflow.js +2 -2
  4. package/docs/cookbook/{common-patterns → advanced}/child-workflows.mdx +1 -1
  5. package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
  6. package/docs/cookbook/advanced/meta.json +2 -3
  7. package/docs/cookbook/advanced/publishing-libraries.mdx +83 -26
  8. package/docs/cookbook/advanced/serializable-steps.mdx +15 -3
  9. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
  10. package/docs/cookbook/agent-patterns/durable-agent.mdx +50 -91
  11. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +148 -171
  12. package/docs/cookbook/agent-patterns/meta.json +1 -7
  13. package/docs/cookbook/common-patterns/batching.mdx +44 -118
  14. package/docs/cookbook/common-patterns/meta.json +4 -4
  15. package/docs/cookbook/common-patterns/saga.mdx +126 -31
  16. package/docs/cookbook/common-patterns/scheduling.mdx +70 -194
  17. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  18. package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
  19. package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
  20. package/docs/cookbook/index.mdx +13 -16
  21. package/docs/cookbook/integrations/ai-sdk.mdx +296 -140
  22. package/docs/cookbook/integrations/chat-sdk.mdx +251 -151
  23. package/docs/cookbook/integrations/sandbox.mdx +469 -81
  24. package/docs/cookbook/meta.json +1 -1
  25. package/docs/foundations/index.mdx +0 -3
  26. package/docs/foundations/meta.json +0 -1
  27. package/docs/foundations/serialization.mdx +1 -1
  28. package/docs/foundations/starting-workflows.mdx +1 -1
  29. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +60 -8
  30. package/docs/migration-guides/migrating-from-inngest.mdx +38 -6
  31. package/docs/migration-guides/migrating-from-temporal.mdx +38 -4
  32. package/docs/migration-guides/migrating-from-trigger-dev.mdx +52 -11
  33. package/package.json +11 -11
  34. package/docs/cookbook/advanced/custom-serialization.mdx +0 -168
  35. package/docs/cookbook/advanced/durable-objects.mdx +0 -148
  36. package/docs/cookbook/advanced/isomorphic-packages.mdx +0 -145
  37. package/docs/cookbook/agent-patterns/stop-workflow.mdx +0 -216
  38. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +0 -255
  39. package/docs/cookbook/agent-patterns/tool-streaming.mdx +0 -181
  40. package/docs/cookbook/common-patterns/content-router.mdx +0 -207
  41. package/docs/cookbook/common-patterns/fan-out.mdx +0 -208
  42. package/docs/foundations/common-patterns.mdx +0 -265
@@ -0,0 +1,205 @@
1
+ ---
2
+ title: Agent Cancellation
3
+ description: Cancel a running agent from the outside — either immediately via run.cancel() or gracefully via a stop signal hook.
4
+ type: guide
5
+ summary: Two patterns for cancelling a running agent — Hard Cancellation via getRun(runId).cancel() for forced termination, or Stop Signal via a hook + Promise.race for a clean exit with cleanup and final stream notification.
6
+ ---
7
+
8
+ Cancel a running agent from the outside — for example, a "Stop" button in a chat UI, an admin cancellation endpoint, or a timeout fallback. Two patterns are available depending on whether you need the agent to exit cleanly or just need the run to stop: **Hard Cancellation** via `getRun(runId).cancel()` for immediate forced termination, or **Stop Signal** via a hook + `Promise.race` for a graceful exit that runs cleanup and notifies streaming clients before returning.
9
+
10
+ ## When to use this
11
+
12
+ * **Chat stop buttons** — let users cancel a long-running agent from the browser
13
+ * **Admin cancellation** — stop an agent from a different process or API
14
+ * **Timeout fallback** — combine with `sleep()` to auto-stop after a deadline
15
+
16
+ ## Choosing an approach
17
+
18
+ Pick the option that matches what your endpoint needs to deliver to the caller:
19
+
20
+ * **Hard Cancellation** — terminates the run immediately with no opportunity for cleanup or client notification. A single line of code, but the workflow throws `WorkflowRunCancelledError` and any streaming clients see an abrupt connection close.
21
+ * **Stop Signal** — the workflow exits as soon as the hook fires, runs any pending cleanup, emits a final `data-stopped` part to the stream so the client can render cleanly, and returns a real result.
22
+
23
+ The trade-offs at a glance:
24
+
25
+ | | Hard Cancellation | Stop Signal |
26
+ | --- | --- | --- |
27
+ | Mechanism | `getRun(runId).cancel()` | Hook + `Promise.race` |
28
+ | Speed to terminate | Immediate | At the next `await` boundary in the workflow |
29
+ | Runs `finally` / cleanup | No | Yes |
30
+ | Final stream notification | No (abrupt close) | Yes (`data-stopped` part) |
31
+ | `run.returnValue` | Throws `WorkflowRunCancelledError` | Returns the workflow's result |
32
+ | Code complexity | One line | Hook + race + signal step |
33
+ | Best for | Stuck or unresponsive runs, forced termination | User-facing stop, admin cancel, timeouts |
34
+
35
+ ## Hard Cancellation
36
+
37
+ Call `.cancel()` on a run to terminate it immediately:
38
+
39
+ ```typescript lineNumbers
40
+ import { getRun } from "workflow/api";
41
+
42
+ export async function POST(
43
+ _request: Request,
44
+ { params }: { params: Promise<{ runId: string }> }
45
+ ) {
46
+ const { runId } = await params;
47
+ await getRun(runId).cancel(); // [!code highlight]
48
+ return Response.json({ success: true });
49
+ }
50
+ ```
51
+
52
+ This is an abrupt termination — the run is stopped mid-step with no opportunity to exit cleanly:
53
+
54
+ * **No cleanup runs** — `finally` blocks, defer-style step cleanup, and any logic after the current step are all skipped
55
+ * **No final notification to the client** — the writable closes abruptly, so a streaming UI just sees the connection drop with no `data-stopped` part to render a clean ending
56
+ * **`run.returnValue` throws** — anyone awaiting the result receives [`WorkflowRunCancelledError`](/docs/api-reference/workflow-errors/workflow-run-cancelled-error) instead of a meaningful payload
57
+ * **Underlying step keeps running** — same caveat as the Stop Signal pattern below: the model stream or HTTP call inside the current step continues to completion in the background
58
+
59
+ Hard Cancellation is the appropriate choice when the run is stuck or unresponsive, has exceeded its expected runtime, or you don't need a clean exit. For everything else — chat stop buttons, admin "stop" actions, timeout fallbacks — you typically want the Stop Signal pattern: the agent finishes its current step, emits a final stream part so the client renders a clean ending, and returns a real result.
60
+
61
+ ## Stop Signal
62
+
63
+ <Callout type="warn">
64
+ **Limitation:** This pattern does not cancel the underlying model stream. The agent step writing to the writable continues running in the background until it completes — tokens generated after the stop signal are still produced (and billed by your model provider). What this pattern *does* is exit the workflow function as soon as the hook fires and emit a `data-stopped` part so the client can stop rendering. For hard cross-process cancellation that signals the inner step to bail out, see [Distributed Abort Controller](/cookbook/advanced/distributed-abort-controller).
65
+ </Callout>
66
+
67
+ ### Example
68
+
69
+ ```typescript lineNumbers
70
+ import { DurableAgent } from "@workflow/ai/agent";
71
+ import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
72
+ import { z } from "zod";
73
+ import type { ModelMessage, UIMessageChunk } from "ai";
74
+
75
+ export const stopHook = defineHook({
76
+ schema: z.object({ reason: z.string().optional() }),
77
+ });
78
+
79
+ async function searchWeb({ query }: { query: string }) {
80
+ "use step";
81
+ await new Promise((r) => setTimeout(r, 1500));
82
+ return { results: [{ title: `${query} - Wikipedia`, snippet: `Overview of ${query}...` }] };
83
+ }
84
+
85
+ async function analyzeData({ topic }: { topic: string }) {
86
+ "use step";
87
+ await new Promise((r) => setTimeout(r, 1200));
88
+ return { summary: `Analysis of ${topic}: significant developments found.`, confidence: 0.85 };
89
+ }
90
+
91
+ async function emitStopSignal(details: { reason?: string }) { // [!code highlight]
92
+ "use step";
93
+ const writer = getWritable<UIMessageChunk>().getWriter();
94
+ try {
95
+ await writer.write({ type: "data-stopped", id: "stop-signal", data: details } as UIMessageChunk);
96
+ } finally {
97
+ writer.releaseLock();
98
+ }
99
+ }
100
+
101
+ export async function stoppableAgent(messages: ModelMessage[]) {
102
+ "use workflow";
103
+
104
+ const { workflowRunId } = getWorkflowMetadata();
105
+ const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
106
+
107
+ const agent = new DurableAgent({
108
+ model: "anthropic/claude-haiku-4.5",
109
+ instructions: "You are a research assistant. Search and analyze data as needed.",
110
+ tools: {
111
+ searchWeb: {
112
+ description: "Search the web for information",
113
+ inputSchema: z.object({ query: z.string() }),
114
+ execute: searchWeb,
115
+ },
116
+ analyzeData: {
117
+ description: "Analyze a piece of data",
118
+ inputSchema: z.object({ topic: z.string() }),
119
+ execute: analyzeData,
120
+ },
121
+ },
122
+ });
123
+
124
+ const result = await Promise.race([ // [!code highlight]
125
+ agent
126
+ .stream({ messages, writable: getWritable<UIMessageChunk>(), maxSteps: 15 })
127
+ .then((r) => ({ type: "complete" as const, messages: r.messages })),
128
+ hook.then(({ reason }) => ({ type: "stopped" as const, reason })), // [!code highlight]
129
+ ]);
130
+
131
+ if (result.type === "stopped") {
132
+ await emitStopSignal({ reason: result.reason }); // [!code highlight]
133
+ }
134
+
135
+ return result;
136
+ }
137
+ ```
138
+
139
+ ### API Route to Trigger Stop
140
+
141
+ ```typescript lineNumbers
142
+ import { stopHook } from "@/workflows/stoppable-agent";
143
+
144
+ export async function POST(
145
+ request: Request,
146
+ { params }: { params: Promise<{ runId: string }> }
147
+ ) {
148
+ const { runId } = await params;
149
+ const { reason } = await request.json();
150
+
151
+ await stopHook.resume(`stop:${runId}`, { // [!code highlight]
152
+ reason: reason || "User requested stop",
153
+ });
154
+
155
+ return Response.json({ success: true });
156
+ }
157
+ ```
158
+
159
+ ### Client Stop Button
160
+
161
+ ```tsx lineNumbers
162
+ "use client";
163
+
164
+ export function StopButton({ runId }: { runId: string }) {
165
+ const handleStop = async () => {
166
+ await fetch(`/api/chat/${runId}/stop`, {
167
+ method: "POST",
168
+ headers: { "Content-Type": "application/json" },
169
+ body: JSON.stringify({ reason: "User clicked stop" }),
170
+ });
171
+ };
172
+
173
+ return (
174
+ <button type="button" onClick={handleStop}>
175
+ Stop Agent
176
+ </button>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ## How it works
182
+
183
+ 1. A hook is created with token `stop:${workflowRunId}` when the workflow starts
184
+ 2. `Promise.race` runs the agent stream and the stop hook concurrently
185
+ 3. When the stop API resumes the hook, the race resolves immediately — the workflow exits
186
+ 4. Before returning, `emitStopSignal` writes a `data-stopped` part to the stream so the client knows the agent was stopped (not just disconnected)
187
+ 5. The client detects `data-stopped` and updates the UI accordingly
188
+
189
+ This is the same pattern used by the [Distributed Abort Controller](/cookbook/advanced/distributed-abort-controller) — race a long-running operation against a hook signal.
190
+
191
+ ## Adapting this
192
+
193
+ * **Add a timeout** — race a third `sleep()` promise to auto-stop after a deadline
194
+ * **Audit logging** — include a `reason` field in the stop schema to record who stopped and why
195
+ * **Cross-process** — the hook token is deterministic, so any process can call `stopHook.resume()` with the run ID
196
+ * **Step limits** — combine with `maxSteps` on the agent to cap execution even without manual stop
197
+ * **Hard Cancellation as a fallback** — wire your stop endpoint to fall back to `getRun(runId).cancel()` if the hook resume errors with `not found` / `expired` (for example, the hook was already consumed). This guarantees the run is terminated even when the Stop Signal path is unavailable.
198
+
199
+ ## Key APIs
200
+
201
+ * [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook for the stop signal
202
+ * [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
203
+ * [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream a stop notification to the client
204
+ * [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — the agent that gets raced against the stop hook
205
+ * [`getRun()`](/docs/api-reference/workflow-api/get-run) — entry point for Hard Cancellation: `getRun(runId).cancel()`
@@ -7,116 +7,61 @@ summary: Convert an AI SDK Agent into a DurableAgent backed by a workflow, with
7
7
 
8
8
  Use this pattern to make any AI SDK agent durable. The agent becomes a workflow, tools become steps, and the framework handles retries, streaming, and state persistence automatically.
9
9
 
10
- ## Pattern
11
-
12
- Replace `Agent` with `DurableAgent`, wrap the function in `"use workflow"`, mark each tool with `"use step"`, and stream output through `getWritable()`.
13
-
14
- ### Simplified
15
-
16
- ```typescript lineNumbers
17
- import { DurableAgent } from "@workflow/ai/agent";
18
- import { getWritable } from "workflow";
19
- import { z } from "zod";
20
- import type { ModelMessage, UIMessageChunk } from "ai";
10
+ ## When to use this
21
11
 
22
- declare function searchFlights(args: { from: string; to: string; date: string }): Promise<{ flights: { id: string; price: number }[] }>; // @setup
23
- declare function bookFlight(args: { flightId: string; passenger: string }): Promise<{ confirmationId: string }>; // @setup
24
-
25
- export async function flightAgent(messages: ModelMessage[]) {
26
- "use workflow";
12
+ - Any AI agent with tool calls that should survive crashes and restarts
13
+ - Agents where tool calls hit external APIs that need automatic retries
14
+ - Long-running agent sessions where losing progress is unacceptable
15
+ - Agents that need per-step observability in the workflow event log
27
16
 
28
- const agent = new DurableAgent({ // [!code highlight]
29
- model: "anthropic/claude-haiku-4.5",
30
- instructions: "You are a helpful flight booking assistant.",
31
- tools: {
32
- searchFlights: {
33
- description: "Search for available flights",
34
- inputSchema: z.object({
35
- from: z.string(),
36
- to: z.string(),
37
- date: z.string(),
38
- }),
39
- execute: searchFlights,
40
- },
41
- bookFlight: {
42
- description: "Book a specific flight",
43
- inputSchema: z.object({
44
- flightId: z.string(),
45
- passenger: z.string(),
46
- }),
47
- execute: bookFlight,
48
- },
49
- },
50
- });
17
+ ## Pattern
51
18
 
52
- await agent.stream({ // [!code highlight]
53
- messages,
54
- writable: getWritable<UIMessageChunk>(),
55
- });
56
- }
57
- ```
19
+ Replace `Agent` with `DurableAgent`, wrap the function in `"use workflow"`, mark each tool with `"use step"`, and stream output through `getWritable()`.
58
20
 
59
- ### Full Implementation
21
+ ### Workflow
60
22
 
61
- ```typescript lineNumbers
23
+ ```typescript
62
24
  import { DurableAgent } from "@workflow/ai/agent";
63
25
  import { getWritable } from "workflow";
64
26
  import { z } from "zod";
65
27
  import type { ModelMessage, UIMessageChunk } from "ai";
66
28
 
67
- // Step: Search flights with full Node.js access and automatic retries
68
- async function searchFlights({
69
- from,
70
- to,
71
- date,
72
- }: {
29
+ async function searchFlights({ from, to, date }: {
73
30
  from: string;
74
31
  to: string;
75
32
  date: string;
76
33
  }) {
77
- "use step";
78
-
79
- const response = await fetch(
34
+ "use step"; // [!code highlight]
35
+ const res = await fetch(
80
36
  `https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
81
37
  );
82
- if (!response.ok) throw new Error(`Search failed: ${response.status}`);
83
- return response.json();
38
+ if (!res.ok) throw new Error(`Search failed: ${res.status}`);
39
+ return res.json();
84
40
  }
85
41
 
86
- // Step: Book a flight retries on transient failures
87
- async function bookFlight({
88
- flightId,
89
- passenger,
90
- }: {
42
+ async function bookFlight({ flightId, passenger }: {
91
43
  flightId: string;
92
44
  passenger: string;
93
45
  }) {
94
- "use step";
95
-
96
- const response = await fetch("https://api.example.com/bookings", {
46
+ "use step"; // [!code highlight]
47
+ const res = await fetch("https://api.example.com/bookings", {
97
48
  method: "POST",
98
49
  headers: { "Content-Type": "application/json" },
99
50
  body: JSON.stringify({ flightId, passenger }),
100
51
  });
101
- if (!response.ok) throw new Error(`Booking failed: ${response.status}`);
102
- return response.json();
52
+ if (!res.ok) throw new Error(`Booking failed: ${res.status}`);
53
+ return res.json();
103
54
  }
104
55
 
105
- // Step: Check flight status
106
- async function checkStatus({ flightId }: { flightId: string }) {
107
- "use step";
108
-
109
- const response = await fetch(
110
- `https://api.example.com/flights/${flightId}/status`
111
- );
112
- return response.json();
56
+ async function checkWeather({ city }: { city: string }) {
57
+ "use step"; // [!code highlight]
58
+ const res = await fetch(`https://api.weather.com/forecast?city=${city}`);
59
+ return res.json();
113
60
  }
114
61
 
115
62
  export async function flightAgent(messages: ModelMessage[]) {
116
63
  "use workflow";
117
64
 
118
- const writable = getWritable<UIMessageChunk>(); // [!code highlight]
119
-
120
65
  const agent = new DurableAgent({ // [!code highlight]
121
66
  model: "anthropic/claude-haiku-4.5",
122
67
  instructions: "You are a helpful flight booking assistant.",
@@ -138,19 +83,19 @@ export async function flightAgent(messages: ModelMessage[]) {
138
83
  }),
139
84
  execute: bookFlight,
140
85
  },
141
- checkStatus: {
142
- description: "Check the current status of a flight",
86
+ checkWeather: {
87
+ description: "Check the weather forecast for a city",
143
88
  inputSchema: z.object({
144
- flightId: z.string().describe("Flight ID to check"),
89
+ city: z.string().describe("City name"),
145
90
  }),
146
- execute: checkStatus,
91
+ execute: checkWeather,
147
92
  },
148
93
  },
149
94
  });
150
95
 
151
96
  const result = await agent.stream({ // [!code highlight]
152
97
  messages,
153
- writable,
98
+ writable: getWritable<UIMessageChunk>(), // [!code highlight]
154
99
  maxSteps: 10,
155
100
  });
156
101
 
@@ -158,22 +103,21 @@ export async function flightAgent(messages: ModelMessage[]) {
158
103
  }
159
104
  ```
160
105
 
161
- ### API Route
106
+ ### API route
162
107
 
163
- ```typescript lineNumbers
164
- import { createUIMessageStreamResponse } from "ai";
165
- import { start } from "workflow/api";
166
- import { flightAgent } from "@/workflows/flight-agent";
108
+ ```typescript
167
109
  import type { UIMessage } from "ai";
168
- import { convertToModelMessages } from "ai";
110
+ import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
111
+ import { start } from "workflow/api";
112
+ import { flightAgent } from "@/app/workflows/flight-agent";
169
113
 
170
114
  export async function POST(req: Request) {
171
115
  const { messages }: { messages: UIMessage[] } = await req.json();
172
- const modelMessages = await convertToModelMessages(messages);
116
+ const modelMessages = await convertToModelMessages(messages); // [!code highlight]
173
117
 
174
118
  const run = await start(flightAgent, [modelMessages]); // [!code highlight]
175
119
 
176
- return createUIMessageStreamResponse({
120
+ return createUIMessageStreamResponse({ // [!code highlight]
177
121
  stream: run.readable,
178
122
  headers: {
179
123
  "x-workflow-run-id": run.runId,
@@ -182,6 +126,21 @@ export async function POST(req: Request) {
182
126
  }
183
127
  ```
184
128
 
129
+ ## How it works
130
+
131
+ 1. **DurableAgent wraps Agent** — same API as AI SDK's `Agent`, but backed by a workflow. If the process crashes, the agent resumes from the last completed step on replay.
132
+ 2. **Tools as steps** — each tool's `execute` function uses `"use step"`, giving it automatic retries, full Node.js access, and an entry in the workflow event log.
133
+ 3. **Streaming** — `getWritable<UIMessageChunk>()` streams the agent's output (text chunks, tool calls, tool results) to the client in real time via `createUIMessageStreamResponse`.
134
+ 4. **maxSteps** — limits the total number of LLM calls the agent can make, preventing runaway tool loops.
135
+
136
+ ## Adapting to your use case
137
+
138
+ - **Change the model** — replace `"anthropic/claude-haiku-4.5"` with any AI Gateway model string (e.g. `"openai/gpt-4o"`, `"anthropic/claude-sonnet-4-5"`).
139
+ - **Add tools** — define a new `"use step"` function with a Zod schema. Each tool automatically gets retries and persistence.
140
+ - **Workflow-level tools** — if a tool needs workflow primitives like `sleep()` or `createHook()`, omit `"use step"` so it runs in the workflow context instead.
141
+ - **Multi-turn** — pass `result.messages` plus new user messages to subsequent `agent.stream()` calls for multi-turn conversations.
142
+ - **Client integration** — use `useChat()` from `@ai-sdk/react` with `WorkflowChatTransport` from `@workflow/ai` for a full chat UI with reconnection support.
143
+
185
144
  ## Key APIs
186
145
 
187
146
  - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function