workflow 5.0.0-beta.0 → 5.0.0-beta.2
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/README.md +4 -4
- package/dist/api-workflow.d.ts +1 -3
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -6
- package/dist/api.js +1 -1
- package/dist/astro.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtins.js +1 -1
- package/dist/internal/class-serialization.js +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/nest.js +1 -1
- package/dist/next.cjs +4 -2
- package/dist/next.d.cts +1 -1
- package/dist/next.d.cts.map +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/stdlib.js +1 -1
- package/dist/sveltekit.js +1 -1
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/vite.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/ai/resumable-streams.mdx +1 -1
- package/docs/api-reference/workflow/create-webhook.mdx +37 -18
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
- package/docs/api-reference/workflow-ai/index.mdx +0 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
- package/docs/api-reference/workflow-api/get-world.mdx +6 -6
- package/docs/api-reference/workflow-api/index.mdx +1 -1
- package/docs/api-reference/workflow-api/world/index.mdx +2 -2
- package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
- package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
- package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
- package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
- package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
- package/docs/cookbook/advanced/durable-objects.mdx +148 -0
- package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
- package/docs/cookbook/advanced/meta.json +10 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
- package/docs/cookbook/agent-patterns/meta.json +10 -0
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
- package/docs/cookbook/common-patterns/batching.mdx +179 -0
- package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
- package/docs/cookbook/common-patterns/content-router.mdx +207 -0
- package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
- package/docs/cookbook/common-patterns/meta.json +15 -0
- package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
- package/docs/cookbook/common-patterns/saga.mdx +152 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/index.mdx +41 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +128 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +45 -43
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +10 -5
- package/docs/deploying/world/vercel-world.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +1 -1
- package/docs/getting-started/index.mdx +8 -1
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/next.mdx +24 -0
- package/docs/getting-started/python.mdx +165 -0
- package/docs/how-it-works/code-transform.mdx +6 -5
- package/docs/meta.json +1 -0
- package/docs/migration-guides/index.mdx +34 -0
- package/docs/migration-guides/meta.json +9 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
- package/package.json +13 -14
- package/dist/internal/private.d.ts +0 -6
- package/dist/internal/private.d.ts.map +0 -1
- package/dist/internal/private.js +0 -6
|
@@ -0,0 +1,255 @@
|
|
|
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
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Batching & Parallel Processing
|
|
3
|
+
description: Process large collections in parallel batches with failure isolation between groups.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Split items into fixed-size batches, process each batch concurrently with Promise.allSettled, and pace batches with sleep to avoid overloading downstream services.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Use batching when you need to process a large list of items in parallel while controlling concurrency. Items are split into fixed-size batches, each batch runs concurrently, and failures in one batch don't affect others.
|
|
9
|
+
|
|
10
|
+
## When to use this
|
|
11
|
+
|
|
12
|
+
- Processing hundreds or thousands of items (orders, images, records)
|
|
13
|
+
- Calling rate-limited APIs where you need to control concurrency
|
|
14
|
+
- Any fan-out where you want failure isolation between groups
|
|
15
|
+
|
|
16
|
+
## Pattern
|
|
17
|
+
|
|
18
|
+
The workflow splits items into chunks and processes each chunk with `Promise.allSettled()`. A `sleep()` between chunks prevents overloading downstream services.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { sleep } from "workflow";
|
|
22
|
+
|
|
23
|
+
declare function processItem(item: string): Promise<{ item: string; ok: boolean }>; // @setup
|
|
24
|
+
|
|
25
|
+
export async function processBatch(items: string[], batchSize: number = 5) {
|
|
26
|
+
"use workflow";
|
|
27
|
+
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
31
|
+
const batch = items.slice(i, i + batchSize);
|
|
32
|
+
|
|
33
|
+
// Run batch in parallel -- failures are isolated
|
|
34
|
+
const outcomes = await Promise.allSettled( // [!code highlight]
|
|
35
|
+
batch.map((item) => processItem(item))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
for (let j = 0; j < outcomes.length; j++) {
|
|
39
|
+
const outcome = outcomes[j];
|
|
40
|
+
results.push(
|
|
41
|
+
outcome.status === "fulfilled"
|
|
42
|
+
? outcome.value
|
|
43
|
+
: { item: batch[j], ok: false, error: String(outcome.reason) }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Pace between batches to avoid overload
|
|
48
|
+
if (i + batchSize < items.length) {
|
|
49
|
+
await sleep("1s"); // [!code highlight]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
54
|
+
return { total: results.length, succeeded, failed: results.length - succeeded };
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step function
|
|
59
|
+
|
|
60
|
+
Each item is processed in its own step, giving it full Node.js access and automatic retries.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
async function processItem(item: string): Promise<{ item: string; ok: boolean }> {
|
|
64
|
+
"use step";
|
|
65
|
+
const res = await fetch(`https://api.example.com/process`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify({ item }),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) throw new Error(`Failed to process ${item}`);
|
|
70
|
+
return { item, ok: true };
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Variations
|
|
75
|
+
|
|
76
|
+
### Scatter-gather
|
|
77
|
+
|
|
78
|
+
When you need results from multiple independent sources before continuing, fan out in parallel and collect all results:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
export async function scatterGather(query: string) {
|
|
82
|
+
"use workflow";
|
|
83
|
+
|
|
84
|
+
const [web, database, cache] = await Promise.allSettled([ // [!code highlight]
|
|
85
|
+
searchWeb(query),
|
|
86
|
+
searchDatabase(query),
|
|
87
|
+
searchCache(query),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
web: web.status === "fulfilled" ? web.value : null,
|
|
92
|
+
database: database.status === "fulfilled" ? database.value : null,
|
|
93
|
+
cache: cache.status === "fulfilled" ? cache.value : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function searchWeb(query: string): Promise<string[]> {
|
|
98
|
+
"use step";
|
|
99
|
+
// Full Node.js access -- call external APIs
|
|
100
|
+
const res = await fetch(`https://search.example.com?q=${query}`);
|
|
101
|
+
return res.json();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function searchDatabase(query: string): Promise<string[]> {
|
|
105
|
+
"use step";
|
|
106
|
+
// Query your database
|
|
107
|
+
return [`db-result-for-${query}`];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function searchCache(query: string): Promise<string[]> {
|
|
111
|
+
"use step";
|
|
112
|
+
return [`cached-result-for-${query}`];
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## In-step concurrency control
|
|
117
|
+
|
|
118
|
+
When you need to process many items against a rate-limited API but want the entire operation to be a single atomic step, batch the work inside the step itself. This keeps the event log clean (one step instead of hundreds) while still controlling concurrency.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
async function processConcurrently<T>(
|
|
122
|
+
items: string[],
|
|
123
|
+
processor: (item: string) => Promise<T>,
|
|
124
|
+
maxConcurrent: number = 5,
|
|
125
|
+
): Promise<T[]> {
|
|
126
|
+
"use step";
|
|
127
|
+
const results: T[] = [];
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < items.length; i += maxConcurrent) {
|
|
130
|
+
const batch = items.slice(i, i + maxConcurrent);
|
|
131
|
+
const batchResults = await Promise.all(batch.map(processor)); // [!code highlight]
|
|
132
|
+
results.push(...batchResults);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Usage in a workflow:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
declare function processConcurrently<T>(items: string[], processor: (item: string) => Promise<T>, maxConcurrent?: number): Promise<T[]>; // @setup
|
|
143
|
+
|
|
144
|
+
export async function moderateImages(imageUrls: string[]) {
|
|
145
|
+
"use workflow";
|
|
146
|
+
|
|
147
|
+
const results = await processConcurrently(
|
|
148
|
+
imageUrls,
|
|
149
|
+
async (url) => {
|
|
150
|
+
const res = await fetch("https://api.example.com/moderate", {
|
|
151
|
+
method: "POST",
|
|
152
|
+
body: JSON.stringify({ url }),
|
|
153
|
+
});
|
|
154
|
+
return res.json();
|
|
155
|
+
},
|
|
156
|
+
3, // max 3 concurrent API calls
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return { total: results.length, results };
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**When to use in-step batching vs workflow-level batching:**
|
|
164
|
+
- **Workflow-level** (the pattern above): Each item is its own step with independent retries and failure isolation. Use when items are independent and individual failures should be retried.
|
|
165
|
+
- **In-step**: All items are processed in one step. Use when the items are tightly coupled (e.g., moderating all thumbnails for a single video) or when you want to minimize step overhead for large item counts.
|
|
166
|
+
|
|
167
|
+
## Tips
|
|
168
|
+
|
|
169
|
+
- **Use `Promise.allSettled` over `Promise.all`** when you want to continue even if some items fail. `Promise.all` rejects on the first failure; `allSettled` waits for everything and tells you what failed.
|
|
170
|
+
- **Tune batch size to your downstream API limits.** If the API allows 10 concurrent requests, use `batchSize: 10`.
|
|
171
|
+
- **Add pacing with `sleep()`** between batches to respect rate limits. The sleep is durable -- it survives cold starts.
|
|
172
|
+
- **Each `processItem` call is an independent step.** If one fails, it retries up to 3 times without affecting other items in the batch.
|
|
173
|
+
|
|
174
|
+
## Key APIs
|
|
175
|
+
|
|
176
|
+
- [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
|
|
177
|
+
- [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
|
|
178
|
+
- [`sleep()`](/docs/api-reference/workflow/sleep) -- pacing delay between batches
|
|
179
|
+
- [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) -- runs items in parallel, isolating failures
|