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.
Files changed (86) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +1 -3
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -6
  5. package/dist/api.js +1 -1
  6. package/dist/astro.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/internal/builtins.js +1 -1
  9. package/dist/internal/class-serialization.js +1 -1
  10. package/dist/internal/errors.js +1 -1
  11. package/dist/nest.js +1 -1
  12. package/dist/next.cjs +4 -2
  13. package/dist/next.d.cts +1 -1
  14. package/dist/next.d.cts.map +1 -1
  15. package/dist/nitro.js +1 -1
  16. package/dist/nuxt.js +1 -1
  17. package/dist/observability.d.ts +1 -1
  18. package/dist/observability.js +2 -2
  19. package/dist/runtime.js +1 -1
  20. package/dist/stdlib.js +1 -1
  21. package/dist/sveltekit.js +1 -1
  22. package/dist/typescript-plugin.cjs +1 -1
  23. package/dist/vite.js +1 -1
  24. package/dist/workflow.js +1 -1
  25. package/docs/ai/resumable-streams.mdx +1 -1
  26. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  27. package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
  28. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  29. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  30. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  31. package/docs/api-reference/workflow-api/get-world.mdx +6 -6
  32. package/docs/api-reference/workflow-api/index.mdx +1 -1
  33. package/docs/api-reference/workflow-api/world/index.mdx +2 -2
  34. package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
  35. package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
  36. package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
  37. package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
  38. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  39. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  40. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  41. package/docs/cookbook/advanced/meta.json +10 -0
  42. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  43. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  44. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  45. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  46. package/docs/cookbook/agent-patterns/meta.json +10 -0
  47. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  48. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  49. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  50. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  51. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  52. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  53. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  54. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  55. package/docs/cookbook/common-patterns/meta.json +15 -0
  56. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  57. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  58. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  59. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  60. package/docs/cookbook/index.mdx +41 -0
  61. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  62. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  63. package/docs/cookbook/integrations/meta.json +4 -0
  64. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  65. package/docs/cookbook/meta.json +5 -0
  66. package/docs/deploying/building-a-world.mdx +45 -43
  67. package/docs/deploying/world/local-world.mdx +1 -1
  68. package/docs/deploying/world/postgres-world.mdx +10 -5
  69. package/docs/deploying/world/vercel-world.mdx +1 -1
  70. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  71. package/docs/getting-started/index.mdx +8 -1
  72. package/docs/getting-started/meta.json +2 -1
  73. package/docs/getting-started/next.mdx +24 -0
  74. package/docs/getting-started/python.mdx +165 -0
  75. package/docs/how-it-works/code-transform.mdx +6 -5
  76. package/docs/meta.json +1 -0
  77. package/docs/migration-guides/index.mdx +34 -0
  78. package/docs/migration-guides/meta.json +9 -0
  79. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  80. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  81. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  82. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  83. package/package.json +13 -14
  84. package/dist/internal/private.d.ts +0 -6
  85. package/dist/internal/private.d.ts.map +0 -1
  86. 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