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,255 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Tool Orchestration
|
|
3
|
-
description: Choose between step-level and workflow-level tools, or combine both for complex tool implementations.
|
|
4
|
-
type: guide
|
|
5
|
-
summary: Implement tools as steps for retries and I/O, at the workflow level for sleep and hooks, or combine both.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
Use this pattern to understand when to implement a tool as a step, at the workflow level, or as a combination. The choice depends on whether the tool needs Node.js I/O (step), workflow primitives like `sleep()` and hooks (workflow level), or both.
|
|
9
|
-
|
|
10
|
-
## Pattern
|
|
11
|
-
|
|
12
|
-
Tools marked with `"use step"` get automatic retries and full Node.js access but cannot use `sleep()` or hooks. Tools without `"use step"` run in the workflow context and can use workflow primitives but cannot perform side effects directly. Combine both by having a workflow-level tool call into steps for I/O.
|
|
13
|
-
|
|
14
|
-
### Step-Level vs Workflow-Level
|
|
15
|
-
|
|
16
|
-
| Capability | Step (`"use step"`) | Workflow Level |
|
|
17
|
-
|------------|---------------------|----------------|
|
|
18
|
-
| `getWritable()` | Yes | Yes |
|
|
19
|
-
| Automatic retries | Yes | No |
|
|
20
|
-
| Side effects (fetch, DB) | Yes | No |
|
|
21
|
-
| `sleep()` | No | Yes |
|
|
22
|
-
| `createHook()` / `createWebhook()` | No | Yes |
|
|
23
|
-
|
|
24
|
-
### Simplified
|
|
25
|
-
|
|
26
|
-
```typescript lineNumbers
|
|
27
|
-
import { DurableAgent } from "@workflow/ai/agent";
|
|
28
|
-
import { sleep, getWritable } from "workflow";
|
|
29
|
-
import { z } from "zod";
|
|
30
|
-
import type { UIMessageChunk } from "ai";
|
|
31
|
-
|
|
32
|
-
// Step-level tool: I/O with retries
|
|
33
|
-
async function fetchWeather({ city }: { city: string }) {
|
|
34
|
-
"use step";
|
|
35
|
-
const res = await fetch(`https://api.weather.com?city=${city}`);
|
|
36
|
-
return res.json();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Workflow-level tool: uses sleep()
|
|
40
|
-
async function scheduleReminder({ delayMs }: { delayMs: number }) {
|
|
41
|
-
// No "use step" — sleep() requires workflow context
|
|
42
|
-
await sleep(delayMs); // [!code highlight]
|
|
43
|
-
return { message: `Reminder fired after ${delayMs}ms` };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Combined: workflow-level orchestration calling into steps
|
|
47
|
-
async function fetchWithDelay({ url, delayMs }: { url: string; delayMs: number }) {
|
|
48
|
-
const result = await doFetch(url); // Step handles I/O // [!code highlight]
|
|
49
|
-
await sleep(delayMs); // Workflow handles sleep // [!code highlight]
|
|
50
|
-
return result;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function doFetch(url: string) {
|
|
54
|
-
"use step";
|
|
55
|
-
const res = await fetch(url);
|
|
56
|
-
return res.json();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function assistantAgent(userMessage: string) {
|
|
60
|
-
"use workflow";
|
|
61
|
-
|
|
62
|
-
const agent = new DurableAgent({
|
|
63
|
-
model: "anthropic/claude-haiku-4.5",
|
|
64
|
-
tools: {
|
|
65
|
-
fetchWeather: {
|
|
66
|
-
description: "Get weather for a city",
|
|
67
|
-
inputSchema: z.object({ city: z.string() }),
|
|
68
|
-
execute: fetchWeather,
|
|
69
|
-
},
|
|
70
|
-
scheduleReminder: {
|
|
71
|
-
description: "Set a reminder after a delay",
|
|
72
|
-
inputSchema: z.object({ delayMs: z.number() }),
|
|
73
|
-
execute: scheduleReminder,
|
|
74
|
-
},
|
|
75
|
-
fetchWithDelay: {
|
|
76
|
-
description: "Fetch a URL then wait before returning",
|
|
77
|
-
inputSchema: z.object({ url: z.string(), delayMs: z.number() }),
|
|
78
|
-
execute: fetchWithDelay,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
await agent.stream({ // [!code highlight]
|
|
84
|
-
messages: [{ role: "user", content: userMessage }],
|
|
85
|
-
writable: getWritable<UIMessageChunk>(),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Full Implementation
|
|
91
|
-
|
|
92
|
-
```typescript lineNumbers
|
|
93
|
-
import { DurableAgent } from "@workflow/ai/agent";
|
|
94
|
-
import { sleep, createWebhook, getWritable } from "workflow";
|
|
95
|
-
import { z } from "zod";
|
|
96
|
-
import type { UIMessageChunk } from "ai";
|
|
97
|
-
|
|
98
|
-
// --- Step-level tools: I/O with retries ---
|
|
99
|
-
|
|
100
|
-
async function searchDatabase({ query }: { query: string }) {
|
|
101
|
-
"use step";
|
|
102
|
-
|
|
103
|
-
const response = await fetch(`https://api.example.com/search?q=${query}`);
|
|
104
|
-
if (!response.ok) throw new Error(`Search failed: ${response.status}`);
|
|
105
|
-
return response.json();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function sendNotification({
|
|
109
|
-
userId,
|
|
110
|
-
message,
|
|
111
|
-
}: {
|
|
112
|
-
userId: string;
|
|
113
|
-
message: string;
|
|
114
|
-
}) {
|
|
115
|
-
"use step";
|
|
116
|
-
|
|
117
|
-
await fetch("https://api.example.com/notifications", {
|
|
118
|
-
method: "POST",
|
|
119
|
-
headers: { "Content-Type": "application/json" },
|
|
120
|
-
body: JSON.stringify({ userId, message }),
|
|
121
|
-
});
|
|
122
|
-
return { sent: true };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// --- Workflow-level tool: uses sleep ---
|
|
126
|
-
|
|
127
|
-
async function waitThenCheck({
|
|
128
|
-
delayMs,
|
|
129
|
-
endpoint,
|
|
130
|
-
}: {
|
|
131
|
-
delayMs: number;
|
|
132
|
-
endpoint: string;
|
|
133
|
-
}) {
|
|
134
|
-
// No "use step" — workflow context needed for sleep()
|
|
135
|
-
await sleep(delayMs); // [!code highlight]
|
|
136
|
-
// Delegate I/O to a step
|
|
137
|
-
return pollEndpoint(endpoint);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function pollEndpoint(endpoint: string) {
|
|
141
|
-
"use step";
|
|
142
|
-
const res = await fetch(endpoint);
|
|
143
|
-
return res.json();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// --- Workflow-level tool: uses webhook ---
|
|
147
|
-
|
|
148
|
-
async function waitForCallback({ description }: { description: string }) {
|
|
149
|
-
// No "use step" — webhooks are workflow primitives
|
|
150
|
-
const webhook = createWebhook(); // [!code highlight]
|
|
151
|
-
// Log the URL so external systems can call it
|
|
152
|
-
console.log(`Waiting for callback at: ${webhook.url}`);
|
|
153
|
-
|
|
154
|
-
const result = await Promise.race([ // [!code highlight]
|
|
155
|
-
webhook.then((req) => req.json()),
|
|
156
|
-
sleep("1h").then(() => ({ status: "timeout" })),
|
|
157
|
-
]);
|
|
158
|
-
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// --- Combined tool: step I/O + workflow sleep + step I/O ---
|
|
163
|
-
|
|
164
|
-
async function retryWithCooldown({
|
|
165
|
-
url,
|
|
166
|
-
maxAttempts,
|
|
167
|
-
}: {
|
|
168
|
-
url: string;
|
|
169
|
-
maxAttempts: number;
|
|
170
|
-
}) {
|
|
171
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
172
|
-
const result = await attemptFetch(url);
|
|
173
|
-
if (result.success) return result;
|
|
174
|
-
if (i < maxAttempts - 1) {
|
|
175
|
-
await sleep(`${(i + 1) * 5}s`); // Increasing cooldown between attempts // [!code highlight]
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return { success: false, error: "All attempts failed" };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function attemptFetch(url: string) {
|
|
182
|
-
"use step";
|
|
183
|
-
try {
|
|
184
|
-
const res = await fetch(url);
|
|
185
|
-
if (!res.ok) return { success: false, status: res.status };
|
|
186
|
-
return { success: true, data: await res.json() };
|
|
187
|
-
} catch {
|
|
188
|
-
return { success: false, error: "Network error" };
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function orchestrationAgent(userMessage: string) {
|
|
193
|
-
"use workflow";
|
|
194
|
-
|
|
195
|
-
const writable = getWritable<UIMessageChunk>();
|
|
196
|
-
|
|
197
|
-
const agent = new DurableAgent({
|
|
198
|
-
model: "anthropic/claude-haiku-4.5",
|
|
199
|
-
instructions:
|
|
200
|
-
"You are an assistant with access to search, notifications, polling, callbacks, and retry tools.",
|
|
201
|
-
tools: {
|
|
202
|
-
searchDatabase: {
|
|
203
|
-
description: "Search the database",
|
|
204
|
-
inputSchema: z.object({ query: z.string() }),
|
|
205
|
-
execute: searchDatabase,
|
|
206
|
-
},
|
|
207
|
-
sendNotification: {
|
|
208
|
-
description: "Send a notification to a user",
|
|
209
|
-
inputSchema: z.object({
|
|
210
|
-
userId: z.string(),
|
|
211
|
-
message: z.string(),
|
|
212
|
-
}),
|
|
213
|
-
execute: sendNotification,
|
|
214
|
-
},
|
|
215
|
-
waitThenCheck: {
|
|
216
|
-
description: "Wait for a duration then check an endpoint",
|
|
217
|
-
inputSchema: z.object({
|
|
218
|
-
delayMs: z.number().describe("Milliseconds to wait"),
|
|
219
|
-
endpoint: z.string().describe("URL to check after waiting"),
|
|
220
|
-
}),
|
|
221
|
-
execute: waitThenCheck,
|
|
222
|
-
},
|
|
223
|
-
waitForCallback: {
|
|
224
|
-
description: "Create a webhook and wait for an external system to call it",
|
|
225
|
-
inputSchema: z.object({
|
|
226
|
-
description: z.string().describe("What the callback is for"),
|
|
227
|
-
}),
|
|
228
|
-
execute: waitForCallback,
|
|
229
|
-
},
|
|
230
|
-
retryWithCooldown: {
|
|
231
|
-
description: "Fetch a URL with retries and increasing cooldown between attempts",
|
|
232
|
-
inputSchema: z.object({
|
|
233
|
-
url: z.string(),
|
|
234
|
-
maxAttempts: z.number().default(3),
|
|
235
|
-
}),
|
|
236
|
-
execute: retryWithCooldown,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
await agent.stream({ // [!code highlight]
|
|
242
|
-
messages: [{ role: "user", content: userMessage }],
|
|
243
|
-
writable,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
## Key APIs
|
|
249
|
-
|
|
250
|
-
- [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
|
|
251
|
-
- [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries and Node.js access
|
|
252
|
-
- [`sleep()`](/docs/api-reference/workflow/sleep) — durable pause (only in workflow context)
|
|
253
|
-
- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) — wait for external HTTP callbacks (only in workflow context)
|
|
254
|
-
- [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream data from steps
|
|
255
|
-
- [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — agent with mixed step/workflow-level tools
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Tool Streaming
|
|
3
|
-
description: Stream real-time progress updates from tools to the UI while they execute.
|
|
4
|
-
type: guide
|
|
5
|
-
summary: Emit custom data parts from step functions to show incremental results during long-running tool calls.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
Use this pattern when tools take a long time to execute and you want to show progress updates, intermediate results, or status messages in the UI while the tool is still running.
|
|
9
|
-
|
|
10
|
-
## Pattern
|
|
11
|
-
|
|
12
|
-
Inside a step function, call `getWritable<UIMessageChunk>()` to write custom data parts to the same stream the agent uses. These appear as typed data parts in the client's message parts array.
|
|
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 { UIMessageChunk } from "ai";
|
|
21
|
-
|
|
22
|
-
declare function performSearch(query: string): Promise<{ id: string; title: string }[]>; // @setup
|
|
23
|
-
declare function searchWithProgress(args: { query: string }): Promise<any>; // @setup
|
|
24
|
-
|
|
25
|
-
export async function searchAgent(userMessage: string) {
|
|
26
|
-
"use workflow";
|
|
27
|
-
|
|
28
|
-
const agent = new DurableAgent({
|
|
29
|
-
model: "anthropic/claude-haiku-4.5",
|
|
30
|
-
tools: {
|
|
31
|
-
search: {
|
|
32
|
-
description: "Search for items",
|
|
33
|
-
inputSchema: z.object({ query: z.string() }),
|
|
34
|
-
execute: searchWithProgress,
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
await agent.stream({ // [!code highlight]
|
|
40
|
-
messages: [{ role: "user", content: userMessage }],
|
|
41
|
-
writable: getWritable<UIMessageChunk>(),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### Full Implementation
|
|
47
|
-
|
|
48
|
-
```typescript lineNumbers
|
|
49
|
-
import { DurableAgent } from "@workflow/ai/agent";
|
|
50
|
-
import { getWritable } from "workflow";
|
|
51
|
-
import { z } from "zod";
|
|
52
|
-
import type { UIMessageChunk } from "ai";
|
|
53
|
-
|
|
54
|
-
// Custom data part type for the client to render
|
|
55
|
-
interface FoundItemDataPart {
|
|
56
|
-
type: "data-found-item";
|
|
57
|
-
id: string;
|
|
58
|
-
data: {
|
|
59
|
-
title: string;
|
|
60
|
-
score: number;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Step: Search with streaming progress updates
|
|
65
|
-
async function searchWithProgress(
|
|
66
|
-
{ query }: { query: string },
|
|
67
|
-
{ toolCallId }: { toolCallId: string }
|
|
68
|
-
) {
|
|
69
|
-
"use step";
|
|
70
|
-
|
|
71
|
-
const writable = getWritable<UIMessageChunk>(); // [!code highlight]
|
|
72
|
-
const writer = writable.getWriter();
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
// Simulate finding items one at a time
|
|
76
|
-
const items = [
|
|
77
|
-
{ title: "Result A", score: 95 },
|
|
78
|
-
{ title: "Result B", score: 87 },
|
|
79
|
-
{ title: "Result C", score: 72 },
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
for (const item of items) {
|
|
83
|
-
// Simulate search latency
|
|
84
|
-
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
85
|
-
|
|
86
|
-
// Stream each result to the UI as it's found
|
|
87
|
-
await writer.write({ // [!code highlight]
|
|
88
|
-
type: "data-found-item",
|
|
89
|
-
id: `${toolCallId}-${item.title}`,
|
|
90
|
-
data: item,
|
|
91
|
-
} as UIMessageChunk);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
message: `Found ${items.length} results for "${query}"`,
|
|
96
|
-
items,
|
|
97
|
-
};
|
|
98
|
-
} finally {
|
|
99
|
-
writer.releaseLock();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Step: Fetch details for a specific item
|
|
104
|
-
async function getItemDetails({ itemId }: { itemId: string }) {
|
|
105
|
-
"use step";
|
|
106
|
-
|
|
107
|
-
const writable = getWritable<UIMessageChunk>();
|
|
108
|
-
const writer = writable.getWriter();
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
// Emit a transient progress message
|
|
112
|
-
await writer.write({ // [!code highlight]
|
|
113
|
-
type: "data-progress",
|
|
114
|
-
data: { message: `Loading details for ${itemId}...` },
|
|
115
|
-
transient: true,
|
|
116
|
-
} as UIMessageChunk);
|
|
117
|
-
|
|
118
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
119
|
-
|
|
120
|
-
return { itemId, description: "Detailed information", available: true };
|
|
121
|
-
} finally {
|
|
122
|
-
writer.releaseLock();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export async function searchAgent(userMessage: string) {
|
|
127
|
-
"use workflow";
|
|
128
|
-
|
|
129
|
-
const writable = getWritable<UIMessageChunk>();
|
|
130
|
-
|
|
131
|
-
const agent = new DurableAgent({
|
|
132
|
-
model: "anthropic/claude-haiku-4.5",
|
|
133
|
-
instructions: "You help users search for items. Use the search tool first, then get details if asked.",
|
|
134
|
-
tools: {
|
|
135
|
-
search: {
|
|
136
|
-
description: "Search for items matching a query",
|
|
137
|
-
inputSchema: z.object({
|
|
138
|
-
query: z.string().describe("Search query"),
|
|
139
|
-
}),
|
|
140
|
-
execute: searchWithProgress,
|
|
141
|
-
},
|
|
142
|
-
getDetails: {
|
|
143
|
-
description: "Get detailed information about a specific item",
|
|
144
|
-
inputSchema: z.object({
|
|
145
|
-
itemId: z.string().describe("Item ID from search results"),
|
|
146
|
-
}),
|
|
147
|
-
execute: getItemDetails,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
await agent.stream({ // [!code highlight]
|
|
153
|
-
messages: [{ role: "user", content: userMessage }],
|
|
154
|
-
writable,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Client Rendering
|
|
160
|
-
|
|
161
|
-
```tsx lineNumbers
|
|
162
|
-
// In your chat component's message rendering:
|
|
163
|
-
{message.parts.map((part, i) => {
|
|
164
|
-
if (part.type === "data-found-item") {
|
|
165
|
-
const item = part.data as { title: string; score: number };
|
|
166
|
-
return (
|
|
167
|
-
<div key={part.id} className="p-3 bg-muted rounded-md">
|
|
168
|
-
<div className="font-medium">{item.title}</div>
|
|
169
|
-
<div className="text-muted-foreground">Score: {item.score}</div>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
// ... other part types
|
|
174
|
-
})}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Key APIs
|
|
178
|
-
|
|
179
|
-
- [`"use step"`](/docs/api-reference/workflow/use-step) — step functions can write to the stream
|
|
180
|
-
- [`getWritable()`](/docs/api-reference/workflow/get-writable) — access the run's output stream from inside a step
|
|
181
|
-
- [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — agent streams LLM output to the same writable
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Conditional Routing
|
|
3
|
-
description: Inspect a payload and route it to different step handlers based on its content.
|
|
4
|
-
type: guide
|
|
5
|
-
summary: Classify incoming messages and branch to specialized handlers using standard if/else logic in the workflow function.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
Use conditional routing when incoming messages need different processing paths depending on their content. A support ticket about billing goes to the billing handler; a bug report goes to engineering. The workflow inspects the payload and branches with standard JavaScript control flow.
|
|
9
|
-
|
|
10
|
-
## When to use this
|
|
11
|
-
|
|
12
|
-
- Support ticket routing by category
|
|
13
|
-
- Order processing with different flows per product type
|
|
14
|
-
- Event handling where different event types need different logic
|
|
15
|
-
- Any message-driven system where the handler depends on the content
|
|
16
|
-
|
|
17
|
-
## Pattern: Content-based router
|
|
18
|
-
|
|
19
|
-
The workflow classifies the input, then branches with `if`/`else` to call the appropriate step:
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
declare function classifyTicket(ticketId: string, subject: string): Promise<{ ticketType: string }>; // @setup
|
|
23
|
-
declare function handleBilling(ticketId: string): Promise<void>; // @setup
|
|
24
|
-
declare function handleTechnical(ticketId: string): Promise<void>; // @setup
|
|
25
|
-
declare function handleAccount(ticketId: string): Promise<void>; // @setup
|
|
26
|
-
declare function handleFeedback(ticketId: string): Promise<void>; // @setup
|
|
27
|
-
|
|
28
|
-
export async function routeTicket(ticketId: string, subject: string) {
|
|
29
|
-
"use workflow";
|
|
30
|
-
|
|
31
|
-
const { ticketType } = await classifyTicket(ticketId, subject); // [!code highlight]
|
|
32
|
-
|
|
33
|
-
if (ticketType === "billing") { // [!code highlight]
|
|
34
|
-
await handleBilling(ticketId);
|
|
35
|
-
} else if (ticketType === "technical") {
|
|
36
|
-
await handleTechnical(ticketId);
|
|
37
|
-
} else if (ticketType === "account") {
|
|
38
|
-
await handleAccount(ticketId);
|
|
39
|
-
} else {
|
|
40
|
-
await handleFeedback(ticketId);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { ticketId, routedTo: ticketType };
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### Step functions
|
|
48
|
-
|
|
49
|
-
Each handler is a separate `"use step"` function. The classification step can use an LLM, keyword matching, or any logic you need:
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
async function classifyTicket(
|
|
53
|
-
ticketId: string,
|
|
54
|
-
subject: string
|
|
55
|
-
): Promise<{ ticketType: string }> {
|
|
56
|
-
"use step";
|
|
57
|
-
|
|
58
|
-
// Example: simple keyword classification
|
|
59
|
-
// In production, this could call an LLM or ML model
|
|
60
|
-
const lower = subject.toLowerCase();
|
|
61
|
-
if (lower.includes("invoice") || lower.includes("charge") || lower.includes("refund")) {
|
|
62
|
-
return { ticketType: "billing" };
|
|
63
|
-
}
|
|
64
|
-
if (lower.includes("error") || lower.includes("bug") || lower.includes("crash")) {
|
|
65
|
-
return { ticketType: "technical" };
|
|
66
|
-
}
|
|
67
|
-
if (lower.includes("password") || lower.includes("login") || lower.includes("access")) {
|
|
68
|
-
return { ticketType: "account" };
|
|
69
|
-
}
|
|
70
|
-
return { ticketType: "feedback" };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function handleBilling(ticketId: string): Promise<void> {
|
|
74
|
-
"use step";
|
|
75
|
-
// Look up billing records, process refund, etc.
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function handleTechnical(ticketId: string): Promise<void> {
|
|
79
|
-
"use step";
|
|
80
|
-
// Create bug report, notify engineering, etc.
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function handleAccount(ticketId: string): Promise<void> {
|
|
84
|
-
"use step";
|
|
85
|
-
// Reset password, update permissions, etc.
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function handleFeedback(ticketId: string): Promise<void> {
|
|
89
|
-
"use step";
|
|
90
|
-
// Log feedback, notify product team, etc.
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Pattern: Enrichment before routing
|
|
95
|
-
|
|
96
|
-
When downstream handlers need more context than the raw input provides, enrich the message in parallel before routing:
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
export async function enrichAndRoute(email: string) {
|
|
100
|
-
"use workflow";
|
|
101
|
-
|
|
102
|
-
// Step 1: Look up base data
|
|
103
|
-
const contact = await lookupContact(email);
|
|
104
|
-
|
|
105
|
-
// Step 2: Enrich from multiple sources in parallel
|
|
106
|
-
const [crm, social] = await Promise.allSettled([ // [!code highlight]
|
|
107
|
-
fetchCrmData(contact),
|
|
108
|
-
fetchSocialData(contact),
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
const enriched = {
|
|
112
|
-
...contact,
|
|
113
|
-
crm: crm.status === "fulfilled" ? crm.value : null,
|
|
114
|
-
social: social.status === "fulfilled" ? social.value : null,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Step 3: Route based on enriched data
|
|
118
|
-
if (enriched.crm?.segment === "enterprise") { // [!code highlight]
|
|
119
|
-
await routeToEnterpriseSales(enriched);
|
|
120
|
-
} else {
|
|
121
|
-
await routeToSelfServe(enriched);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { email, segment: enriched.crm?.segment ?? "self-serve" };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function lookupContact(email: string): Promise<{ email: string; domain: string }> {
|
|
128
|
-
"use step";
|
|
129
|
-
return { email, domain: email.split("@")[1] ?? "unknown" };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function fetchCrmData(contact: { email: string }): Promise<{ segment: string }> {
|
|
133
|
-
"use step";
|
|
134
|
-
const res = await fetch(`https://crm.example.com/lookup?email=${contact.email}`);
|
|
135
|
-
return res.json();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function fetchSocialData(contact: { email: string }): Promise<{ followers: number }> {
|
|
139
|
-
"use step";
|
|
140
|
-
const res = await fetch(`https://social.example.com/lookup?email=${contact.email}`);
|
|
141
|
-
return res.json();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function routeToEnterpriseSales(enriched: unknown): Promise<void> {
|
|
145
|
-
"use step";
|
|
146
|
-
// Assign to enterprise sales team
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function routeToSelfServe(enriched: unknown): Promise<void> {
|
|
150
|
-
"use step";
|
|
151
|
-
// Add to self-serve onboarding flow
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Pattern: Multiple event sources
|
|
156
|
-
|
|
157
|
-
When a workflow must wait for signals from different systems before proceeding, create one hook per source and use `Promise.all` with a deadline:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { defineHook, sleep } from "workflow";
|
|
161
|
-
|
|
162
|
-
export const orderSignal = defineHook<{ ok: true }>();
|
|
163
|
-
|
|
164
|
-
const SIGNALS = ["payment", "inventory", "fraud"] as const;
|
|
165
|
-
|
|
166
|
-
export async function waitForAllSignals(orderId: string) {
|
|
167
|
-
"use workflow";
|
|
168
|
-
|
|
169
|
-
const hooks = SIGNALS.map((kind) =>
|
|
170
|
-
orderSignal.create({ token: `${kind}:${orderId}` }) // [!code highlight]
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const outcome = await Promise.race([ // [!code highlight]
|
|
174
|
-
Promise.all(hooks).then(() => ({ type: "ready" as const })), // [!code highlight]
|
|
175
|
-
sleep("5m").then(() => ({ type: "timeout" as const })), // [!code highlight]
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
if (outcome.type === "timeout") {
|
|
179
|
-
return { orderId, status: "timeout" };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
await shipOrder(orderId);
|
|
183
|
-
return { orderId, status: "shipped" };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function shipOrder(orderId: string): Promise<void> {
|
|
187
|
-
"use step";
|
|
188
|
-
await fetch(`https://shipping.example.com/ship`, {
|
|
189
|
-
method: "POST",
|
|
190
|
-
body: JSON.stringify({ orderId }),
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Tips
|
|
196
|
-
|
|
197
|
-
- **Workflow functions use standard JavaScript.** `if`/`else`, `switch`, ternaries -- any branching logic works. No special routing DSL needed.
|
|
198
|
-
- **Each handler is an independent step.** This means each gets its own retries, its own error handling, and its own entry in the event log.
|
|
199
|
-
- **Combine with enrichment** when downstream handlers need data from multiple sources. Fan out enrichment with `Promise.allSettled`, then route on the merged result.
|
|
200
|
-
- **Use `defineHook` for event gateways** when the routing decision depends on external signals arriving asynchronously.
|
|
201
|
-
|
|
202
|
-
## Key APIs
|
|
203
|
-
|
|
204
|
-
- [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- marks the orchestrator function
|
|
205
|
-
- [`"use step"`](/docs/api-reference/workflow/use-step) -- marks each handler as a durable step
|
|
206
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook) -- creates hooks for event gateway patterns
|
|
207
|
-
- [`sleep()`](/docs/api-reference/workflow/sleep) -- durable deadline for event gateways
|