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.
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -2
- package/docs/cookbook/{common-patterns → advanced}/child-workflows.mdx +1 -1
- package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
- package/docs/cookbook/advanced/meta.json +2 -3
- package/docs/cookbook/advanced/publishing-libraries.mdx +83 -26
- package/docs/cookbook/advanced/serializable-steps.mdx +15 -3
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +50 -91
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +148 -171
- package/docs/cookbook/agent-patterns/meta.json +1 -7
- package/docs/cookbook/common-patterns/batching.mdx +44 -118
- package/docs/cookbook/common-patterns/meta.json +4 -4
- package/docs/cookbook/common-patterns/saga.mdx +126 -31
- package/docs/cookbook/common-patterns/scheduling.mdx +70 -194
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
- package/docs/cookbook/index.mdx +13 -16
- package/docs/cookbook/integrations/ai-sdk.mdx +296 -140
- package/docs/cookbook/integrations/chat-sdk.mdx +251 -151
- package/docs/cookbook/integrations/sandbox.mdx +469 -81
- package/docs/cookbook/meta.json +1 -1
- package/docs/foundations/index.mdx +0 -3
- package/docs/foundations/meta.json +0 -1
- package/docs/foundations/serialization.mdx +1 -1
- package/docs/foundations/starting-workflows.mdx +1 -1
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +60 -8
- package/docs/migration-guides/migrating-from-inngest.mdx +38 -6
- package/docs/migration-guides/migrating-from-temporal.mdx +38 -4
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +52 -11
- package/package.json +11 -11
- package/docs/cookbook/advanced/custom-serialization.mdx +0 -168
- package/docs/cookbook/advanced/durable-objects.mdx +0 -148
- package/docs/cookbook/advanced/isomorphic-packages.mdx +0 -145
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +0 -216
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +0 -255
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +0 -181
- package/docs/cookbook/common-patterns/content-router.mdx +0 -207
- package/docs/cookbook/common-patterns/fan-out.mdx +0 -208
- 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
|