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
@@ -1,168 +0,0 @@
1
- ---
2
- title: Custom Serialization
3
- description: Make class instances serializable across workflow boundaries using the WORKFLOW_SERIALIZE and WORKFLOW_DESERIALIZE symbol protocol.
4
- type: guide
5
- summary: Implement the WORKFLOW_SERIALIZE and WORKFLOW_DESERIALIZE symbol protocol on classes so instances survive serialization when passed between workflow and step functions.
6
- ---
7
-
8
- <Callout>
9
- This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
- </Callout>
11
-
12
- ## The Problem
13
-
14
- Workflow functions run inside a sandboxed VM. Every value that crosses a function boundary — step arguments, step return values, workflow inputs — must be [serializable](/docs/foundations/serialization). Plain objects, strings, numbers, and many built-in types (`Date`, `Map`, `Set`, `RegExp`, etc.) work automatically, but **class instances** that don't implement the custom class serialization protocol will throw a serialization error.
15
-
16
- ```typescript lineNumbers
17
- class StorageClient {
18
- constructor(private region: string) {}
19
-
20
- async upload(key: string, body: Uint8Array) {
21
- // ... uses this.region internally
22
- }
23
- }
24
-
25
- export async function processFile(client: StorageClient) {
26
- "use workflow";
27
-
28
- // client fails to serialize — StorageClient doesn't implement custom class serialization
29
- // The runtime throws a serialization error
30
- await uploadStep(client, "output.json", data);
31
- }
32
- ```
33
-
34
- Custom class serialization solves this by teaching the runtime how to convert your class instances to plain data and back.
35
-
36
- ## The WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE Protocol
37
-
38
- The `@workflow/serde` package exports two symbols that act as a custom class serialization protocol. When the workflow runtime encounters a class instance with these symbols, it knows how to convert it to plain data and back.
39
-
40
- {/* @skip-typecheck - @workflow/serde is not mapped in the type-checker */}
41
- ```typescript lineNumbers
42
- import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde";
43
-
44
- class Point {
45
- constructor(public x: number, public y: number) {}
46
-
47
- distanceTo(other: Point): number {
48
- return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
49
- }
50
-
51
- static [WORKFLOW_SERIALIZE](instance: Point) { // [!code highlight]
52
- return { x: instance.x, y: instance.y };
53
- }
54
-
55
- static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { // [!code highlight]
56
- return new Point(data.x, data.y);
57
- }
58
- }
59
- ```
60
-
61
- Both methods must be **static**. `WORKFLOW_SERIALIZE` receives an instance and returns plain serializable data. `WORKFLOW_DESERIALIZE` receives that same data and reconstructs a new instance.
62
-
63
- <Callout type="warn">
64
- Both serialization methods run inside the workflow VM. They must not use Node.js APIs, non-deterministic operations, or network calls. Keep them focused on extracting and reconstructing data.
65
- </Callout>
66
-
67
- ## Automatic Class Registration
68
-
69
- For the runtime to deserialize a class, the class must be registered in a global registry with a stable `classId`. The SWC compiler plugin handles this automatically — when it detects a class with both `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` static methods, it generates registration code at build time.
70
-
71
- This means you only need to implement the two symbol methods. The compiler assigns a deterministic `classId` based on the file path and class name, and registers it in the global `Symbol.for("workflow-class-registry")` registry.
72
-
73
- <Callout type="info">
74
- No manual registration is required for classes defined in your workflow files. The SWC plugin detects the serialization symbols and generates the registration automatically at build time.
75
- </Callout>
76
-
77
- ## Full Example: A Workflow-Safe Storage Client
78
-
79
- Here's a complete example of a storage client class that survives serialization across workflow boundaries. This pattern is useful when you need an object with methods to be passed as a workflow input or returned from a step.
80
-
81
- ```typescript lineNumbers
82
- import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde";
83
-
84
- interface StorageClientOptions {
85
- region: string;
86
- bucket: string;
87
- accessKeyId?: string;
88
- secretAccessKey?: string;
89
- }
90
-
91
- export class WorkflowStorageClient {
92
- private readonly region: string;
93
- private readonly bucket: string;
94
- private readonly accessKeyId?: string;
95
- private readonly secretAccessKey?: string;
96
-
97
- constructor(options: StorageClientOptions) {
98
- this.region = options.region;
99
- this.bucket = options.bucket;
100
- this.accessKeyId = options.accessKeyId;
101
- this.secretAccessKey = options.secretAccessKey;
102
- }
103
-
104
- async upload(key: string, body: Uint8Array) {
105
- "use step";
106
- const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
107
- const client = new S3Client({
108
- region: this.region,
109
- credentials: this.accessKeyId
110
- ? { accessKeyId: this.accessKeyId, secretAccessKey: this.secretAccessKey! }
111
- : undefined,
112
- });
113
- await client.send(
114
- new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: body })
115
- );
116
- }
117
-
118
- async getSignedUrl(key: string): Promise<string> {
119
- "use step";
120
- const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
121
- const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
122
- const client = new S3Client({ region: this.region });
123
- return getSignedUrl(client, new GetObjectCommand({ Bucket: this.bucket, Key: key }));
124
- }
125
-
126
- // --- Serialization protocol ---
127
-
128
- static [WORKFLOW_SERIALIZE](instance: WorkflowStorageClient): StorageClientOptions { // [!code highlight]
129
- return {
130
- region: instance.region,
131
- bucket: instance.bucket,
132
- accessKeyId: instance.accessKeyId,
133
- secretAccessKey: instance.secretAccessKey,
134
- };
135
- }
136
-
137
- static [WORKFLOW_DESERIALIZE]( // [!code highlight]
138
- data: StorageClientOptions
139
- ): WorkflowStorageClient {
140
- return new WorkflowStorageClient(data);
141
- }
142
- }
143
- ```
144
-
145
- Now this client can be passed into a workflow and used directly:
146
-
147
- ```typescript lineNumbers
148
- import { WorkflowStorageClient } from "./storage-client";
149
-
150
- export async function processUpload(
151
- client: WorkflowStorageClient,
152
- data: Uint8Array
153
- ) {
154
- "use workflow";
155
-
156
- // client is a real WorkflowStorageClient with working methods
157
- await client.upload("output/result.json", data); // [!code highlight]
158
- const url = await client.getSignedUrl("output/result.json"); // [!code highlight]
159
- return { url };
160
- }
161
- ```
162
-
163
- ## Key APIs
164
-
165
- - [`WORKFLOW_SERIALIZE`](/docs/api-reference/workflow-serde/workflow-serialize) — symbol for the static serialization method
166
- - [`WORKFLOW_DESERIALIZE`](/docs/api-reference/workflow-serde/workflow-deserialize) — symbol for the static deserialization method
167
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks a function for extraction and serialization
168
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
@@ -1,148 +0,0 @@
1
- ---
2
- title: Durable Objects
3
- description: Model long-lived stateful entities as workflows that persist state across requests.
4
- type: guide
5
- summary: Build a durable counter or session object whose state survives restarts by using a workflow's event log as the persistence layer.
6
- ---
7
-
8
- <Callout>
9
- This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
- </Callout>
11
-
12
- ## The Idea
13
-
14
- A workflow's event log already records every step result and replays them to reconstruct state. This is the same property that makes an "object" durable — its fields survive cold starts, crashes, and redeployments. Instead of using a workflow to model a *process*, you can use one to model an *entity* with methods.
15
-
16
- Each "method call" is a hook that the object's workflow loop awaits. External callers resume the hook with a payload describing the operation. The workflow applies the operation, updates its internal state, and waits for the next call.
17
-
18
- ## Pattern: Durable Counter
19
-
20
- A counter that persists its value without a database. Each increment/decrement is recorded in the event log.
21
-
22
- ```typescript lineNumbers
23
- import { defineHook, getWorkflowMetadata } from "workflow";
24
- import { z } from "zod";
25
-
26
- const counterAction = defineHook({ // [!code highlight]
27
- schema: z.object({
28
- type: z.enum(["increment", "decrement", "get"]),
29
- amount: z.number().default(1),
30
- }),
31
- });
32
-
33
- export async function durableCounter() {
34
- "use workflow";
35
-
36
- let count = 0;
37
- const { workflowRunId } = getWorkflowMetadata();
38
-
39
- while (true) {
40
- const hook = counterAction.create({ token: `counter:${workflowRunId}` });
41
- const action = await hook; // [!code highlight]
42
-
43
- switch (action.type) {
44
- case "increment":
45
- count += action.amount;
46
- await recordState(count);
47
- break;
48
- case "decrement":
49
- count -= action.amount;
50
- await recordState(count);
51
- break;
52
- case "get":
53
- await emitValue(count);
54
- break;
55
- }
56
- }
57
- }
58
-
59
- async function recordState(count: number) {
60
- "use step";
61
- // Step records the state transition in the event log.
62
- // On replay, the step result restores `count` without re-executing.
63
- return count;
64
- }
65
-
66
- async function emitValue(count: number) {
67
- "use step";
68
- return { count };
69
- }
70
- ```
71
-
72
- ### Calling the Object
73
-
74
- From an API route, resume the hook to "invoke a method" on the durable object:
75
-
76
- ```typescript lineNumbers
77
- import { resumeHook } from "workflow/api";
78
-
79
- export async function POST(request: Request) {
80
- const { runId, type, amount } = await request.json();
81
- await resumeHook(`counter:${runId}`, { type, amount }); // [!code highlight]
82
- return Response.json({ ok: true });
83
- }
84
- ```
85
-
86
- ## Pattern: Durable Session
87
-
88
- A chat session where conversation history is the durable state. Each user message is a hook event; the workflow accumulates messages and generates responses.
89
-
90
- ```typescript lineNumbers
91
- import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
92
- import { DurableAgent } from "@workflow/ai/agent";
93
- import { anthropic } from "@workflow/ai/anthropic";
94
- import { z } from "zod";
95
- import type { UIMessageChunk, ModelMessage } from "ai";
96
-
97
- const messageHook = defineHook({ // [!code highlight]
98
- schema: z.object({
99
- role: z.literal("user"),
100
- content: z.string(),
101
- }),
102
- });
103
-
104
- export async function durableSession() {
105
- "use workflow";
106
-
107
- const writable = getWritable<UIMessageChunk>();
108
- const { workflowRunId: runId } = getWorkflowMetadata();
109
- const messages: ModelMessage[] = [];
110
-
111
- const agent = new DurableAgent({
112
- model: anthropic("claude-sonnet-4-20250514"),
113
- instructions: "You are a helpful assistant.",
114
- });
115
-
116
- while (true) {
117
- const hook = messageHook.create({ token: `session:${runId}` });
118
- const userMessage = await hook; // [!code highlight]
119
-
120
- messages.push({
121
- role: userMessage.role,
122
- content: userMessage.content,
123
- });
124
-
125
- await agent.stream({ messages, writable });
126
- }
127
- }
128
- ```
129
-
130
- ## When to Use This
131
-
132
- - **Entity-per-workflow**: Each user, document, or device gets its own workflow run. The run ID is the entity ID.
133
- - **No external database needed**: State lives in the event log. Reads replay from the log; writes append to it.
134
- - **Automatic consistency**: Only one execution runs at a time per workflow run, so there are no race conditions on the entity's state.
135
-
136
- ## Trade-offs
137
-
138
- - **Read latency**: Accessing current state requires replaying the event log (or caching the last known state in a step result).
139
- - **Not a replacement for databases**: If you need to query across entities (e.g., "all counters above 100"), you still need a database. Durable objects are for single-entity state.
140
- - **Log growth**: Long-lived objects accumulate large event logs. Consider periodic "snapshot" steps that checkpoint the full state.
141
-
142
- ## Key APIs
143
-
144
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
145
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
146
- - [`defineHook`](/docs/api-reference/workflow/define-hook) — type-safe hook for receiving external method calls
147
- - [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
148
- - [`resumeHook`](/docs/api-reference/workflow-api/resume-hook) — invoke a method on the durable object from an API route
@@ -1,145 +0,0 @@
1
- ---
2
- title: Isomorphic Packages
3
- description: Publish reusable workflow packages that work both inside and outside the workflow runtime.
4
- type: guide
5
- summary: Use try/catch around getWorkflowMetadata, dynamic imports, and optional peer dependencies to build libraries that run in workflows and in plain Node.js.
6
- ---
7
-
8
- <Callout>
9
- This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
- </Callout>
11
-
12
- ## The Challenge
13
-
14
- If you're a library author publishing a package that integrates with workflow, your code needs to handle two environments:
15
-
16
- 1. **Inside a workflow run** — `getWorkflowMetadata()` works, `"use step"` directives are transformed, and the full workflow runtime is available.
17
- 2. **Outside a workflow** — your package is imported in a regular Node.js process, a test suite, or a project that doesn't use workflow at all.
18
-
19
- A hard dependency on `workflow` will crash at import time for users who don't have it installed.
20
-
21
- ## Pattern 1: Feature-Detect with `getWorkflowMetadata`
22
-
23
- Use a try/catch to detect whether you're running inside a workflow. This lets you add durable behavior when available and fall back to standard execution otherwise.
24
-
25
- ```typescript lineNumbers
26
- import { getWorkflowMetadata } from "workflow";
27
-
28
- export async function processPayment(amount: number, currency: string) {
29
- "use workflow";
30
-
31
- let runId: string | undefined;
32
- try {
33
- const metadata = getWorkflowMetadata(); // [!code highlight]
34
- runId = metadata.workflowRunId;
35
- } catch {
36
- // Not running inside a workflow — proceed without durability
37
- runId = undefined;
38
- }
39
-
40
- if (runId) {
41
- // Inside a workflow: use the run ID as an idempotency key
42
- return await chargeWithIdempotency(amount, currency, runId); // [!code highlight]
43
- } else {
44
- // Outside a workflow: standard charge
45
- return await chargeStandard(amount, currency);
46
- }
47
- }
48
-
49
- async function chargeWithIdempotency(amount: number, currency: string, idempotencyKey: string) {
50
- "use step";
51
- // Stripe charge with idempotency key from workflow run ID
52
- return { charged: true, amount, currency, idempotencyKey };
53
- }
54
-
55
- async function chargeStandard(amount: number, currency: string) {
56
- "use step";
57
- return { charged: true, amount, currency };
58
- }
59
- ```
60
-
61
- ## Pattern 2: Dynamic Imports
62
-
63
- Avoid importing `workflow` at the top level. Use dynamic `import()` so the module is only loaded when actually needed.
64
-
65
- ```typescript lineNumbers
66
- export async function createDurableTask(name: string, payload: unknown) {
67
- "use workflow";
68
-
69
- let sleep: ((duration: string) => Promise<void>) | undefined;
70
-
71
- try {
72
- const wf = await import("workflow"); // [!code highlight]
73
- sleep = wf.sleep;
74
- } catch {
75
- // workflow not installed — use setTimeout fallback
76
- sleep = undefined;
77
- }
78
-
79
- await executeTask(name, payload);
80
-
81
- if (sleep) {
82
- // Inside workflow: durable sleep that survives restarts
83
- await sleep("5m"); // [!code highlight]
84
- } else {
85
- // Outside workflow: plain timer (not durable)
86
- await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
87
- }
88
-
89
- await sendNotification(name);
90
- }
91
-
92
- async function executeTask(name: string, payload: unknown) {
93
- "use step";
94
- return { executed: true, name, payload };
95
- }
96
-
97
- async function sendNotification(name: string) {
98
- "use step";
99
- return { notified: true, name };
100
- }
101
- ```
102
-
103
- ## Pattern 3: Optional Peer Dependencies
104
-
105
- In your `package.json`, declare `workflow` as an optional peer dependency. This signals to package managers that your library *can* use workflow but doesn't require it.
106
-
107
- ```json
108
- {
109
- "name": "@acme/payments",
110
- "peerDependencies": {
111
- "workflow": ">=1.0.0"
112
- },
113
- "peerDependenciesMeta": {
114
- "workflow": {
115
- "optional": true
116
- }
117
- }
118
- }
119
- ```
120
-
121
- Then guard all workflow imports with dynamic `import()` and try/catch as shown above.
122
-
123
- ## Real-World Examples
124
-
125
- ### Mux AI
126
-
127
- The Mux team published a reusable workflow package for video processing. Their library detects the workflow runtime and falls back to standard async processing when workflow isn't available.
128
-
129
- ### World ID
130
-
131
- World ID's identity verification library uses `getWorkflowMetadata()` to attach run IDs to their human-in-the-loop verification hooks, but the same library works in non-workflow environments for simple verification flows.
132
-
133
- ## Guidelines for Library Authors
134
-
135
- 1. **Never hard-import `workflow` at the top level** if your package should work without it.
136
- 2. **Use `getWorkflowMetadata()` in a try/catch** as the canonical runtime detection pattern.
137
- 3. **Mark `workflow` as an optional peer dependency** in `package.json`.
138
- 4. **Test both paths**: run your test suite with and without the workflow runtime to catch import errors.
139
- 5. **Document the dual behavior**: make it clear in your README which features require workflow and which work standalone.
140
-
141
- ## Key APIs
142
-
143
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
144
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
145
- - [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — runtime detection and run ID access
@@ -1,216 +0,0 @@
1
- ---
2
- title: Stop Workflow
3
- description: Gracefully cancel a running agent workflow using a hook signal.
4
- type: guide
5
- summary: Use a hook as a stop signal to break out of an agent loop and close the stream cleanly.
6
- ---
7
-
8
- Use this pattern when you need to gracefully stop a running agent from the outside — for example, a "Stop" button in a chat UI or an admin cancellation endpoint. The workflow listens for a stop signal via a hook while the agent runs, and breaks out of the loop when the signal arrives.
9
-
10
- ## Pattern
11
-
12
- Create a hook with a known token (the run ID). Listen for a stop signal in a non-blocking `.then()`. In the `prepareStep` callback, check the flag and return `{ toolChoice: "none" }` to prevent further tool calls, causing the agent to generate a final response and exit the loop cleanly.
13
-
14
- ### Simplified
15
-
16
- ```typescript lineNumbers
17
- import { DurableAgent } from "@workflow/ai/agent";
18
- import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
19
- import { z } from "zod";
20
- import type { ModelMessage, UIMessageChunk } from "ai";
21
-
22
- export const stopHook = defineHook({
23
- schema: z.object({ reason: z.string().optional() }),
24
- });
25
-
26
- declare function processQuery(args: { query: string }): Promise<string>; // @setup
27
-
28
- export async function stoppableAgent(messages: ModelMessage[]) {
29
- "use workflow";
30
-
31
- const { workflowRunId } = getWorkflowMetadata();
32
- let stopRequested = false;
33
-
34
- const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
35
- hook.then(() => { stopRequested = true; }); // [!code highlight]
36
-
37
- const agent = new DurableAgent({
38
- model: "anthropic/claude-haiku-4.5",
39
- tools: {
40
- processQuery: {
41
- description: "Process a query",
42
- inputSchema: z.object({ query: z.string() }),
43
- execute: processQuery,
44
- },
45
- },
46
- });
47
-
48
- const result = await agent.stream({
49
- messages,
50
- writable: getWritable<UIMessageChunk>(),
51
- prepareStep: () => { // [!code highlight]
52
- if (stopRequested) return { toolChoice: "none" }; // [!code highlight]
53
- return {};
54
- },
55
- });
56
-
57
- return { messages: result.messages, stopped: stopRequested };
58
- }
59
- ```
60
-
61
- ### Full Implementation
62
-
63
- ```typescript lineNumbers
64
- import { DurableAgent } from "@workflow/ai/agent";
65
- import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
66
- import { z } from "zod";
67
- import type { ModelMessage, UIMessageChunk } from "ai";
68
-
69
- // Hook to signal the workflow to stop
70
- export const stopHook = defineHook({
71
- schema: z.object({
72
- reason: z.string().optional(),
73
- }),
74
- });
75
-
76
- // Step: Search the web
77
- async function searchWeb({ query }: { query: string }) {
78
- "use step";
79
-
80
- await new Promise((resolve) => setTimeout(resolve, 1000));
81
- return { results: [`Result for "${query}"`] };
82
- }
83
-
84
- // Step: Analyze data
85
- async function analyzeData({ data }: { data: string }) {
86
- "use step";
87
-
88
- await new Promise((resolve) => setTimeout(resolve, 800));
89
- return { analysis: `Analysis of: ${data}` };
90
- }
91
-
92
- // Step: Write the final close marker to the stream
93
- async function closeStream() {
94
- "use step";
95
-
96
- const writable = getWritable<UIMessageChunk>();
97
- const writer = writable.getWriter();
98
- try {
99
- await writer.write({ type: "finish" } as UIMessageChunk);
100
- } finally {
101
- writer.releaseLock();
102
- }
103
- await writable.close();
104
- }
105
-
106
- export async function stoppableAgent(messages: ModelMessage[]) {
107
- "use workflow";
108
-
109
- const { workflowRunId } = getWorkflowMetadata();
110
- const writable = getWritable<UIMessageChunk>();
111
-
112
- // Listen for stop signal using a non-blocking hook
113
- let stopRequested = false;
114
- let stopReason: string | undefined;
115
-
116
- const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
117
- hook.then(({ reason }) => { // [!code highlight]
118
- stopRequested = true;
119
- stopReason = reason;
120
- });
121
-
122
- const agent = new DurableAgent({
123
- model: "anthropic/claude-haiku-4.5",
124
- instructions: "You are a research assistant. Search and analyze data as needed.",
125
- tools: {
126
- searchWeb: {
127
- description: "Search the web for information",
128
- inputSchema: z.object({ query: z.string() }),
129
- execute: searchWeb,
130
- },
131
- analyzeData: {
132
- description: "Analyze a piece of data",
133
- inputSchema: z.object({ data: z.string() }),
134
- execute: analyzeData,
135
- },
136
- },
137
- });
138
-
139
- const result = await agent.stream({
140
- messages,
141
- writable,
142
- preventClose: true,
143
- maxSteps: 20,
144
- prepareStep: ({ stepNumber }) => { // [!code highlight]
145
- // Check stop flag before each agent step.
146
- // Setting toolChoice to "none" prevents tool calls,
147
- // causing the agent to generate a final response and exit.
148
- if (stopRequested) {
149
- return { toolChoice: "none" }; // [!code highlight]
150
- }
151
- return {};
152
- },
153
- });
154
-
155
- // Clean up: close the stream
156
- await closeStream();
157
-
158
- return {
159
- messages: result.messages,
160
- stopped: stopRequested,
161
- stopReason,
162
- stepsCompleted: result.steps.length,
163
- };
164
- }
165
- ```
166
-
167
- ### API Route to Trigger Stop
168
-
169
- ```typescript lineNumbers
170
- import { stopHook } from "@/workflows/stoppable-agent";
171
-
172
- export async function POST(
173
- request: Request,
174
- { params }: { params: Promise<{ runId: string }> }
175
- ) {
176
- const { runId } = await params;
177
- const { reason } = await request.json();
178
-
179
- await stopHook.resume(`stop:${runId}`, { // [!code highlight]
180
- reason: reason || "User requested stop",
181
- });
182
-
183
- return Response.json({ success: true });
184
- }
185
- ```
186
-
187
- ### Client Stop Button
188
-
189
- ```tsx lineNumbers
190
- "use client";
191
-
192
- export function StopButton({ runId }: { runId: string }) {
193
- const handleStop = async () => {
194
- await fetch(`/api/chat/${runId}/stop`, {
195
- method: "POST",
196
- headers: { "Content-Type": "application/json" },
197
- body: JSON.stringify({ reason: "User clicked stop" }),
198
- });
199
- };
200
-
201
- return (
202
- <button type="button" onClick={handleStop}>
203
- Stop Agent
204
- </button>
205
- );
206
- }
207
- ```
208
-
209
- ## Key APIs
210
-
211
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
212
- - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
213
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook for the stop signal
214
- - [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
215
- - [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream output and close cleanly on stop
216
- - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — `prepareStep` callback to check stop flag before each step