workflow 5.0.0-beta.1 → 5.0.0-beta.3

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 (72) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +1 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -2
  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 +1 -1
  13. package/dist/nitro.js +1 -1
  14. package/dist/nuxt.js +1 -1
  15. package/dist/observability.js +1 -1
  16. package/dist/runtime.js +1 -1
  17. package/dist/stdlib.js +1 -1
  18. package/dist/sveltekit.js +1 -1
  19. package/dist/typescript-plugin.cjs +1 -1
  20. package/dist/vite.js +1 -1
  21. package/dist/workflow.js +1 -1
  22. package/docs/ai/resumable-streams.mdx +1 -1
  23. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  24. package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
  25. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  26. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  27. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  28. package/docs/cookbook/advanced/child-workflows.mdx +372 -0
  29. package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
  30. package/docs/cookbook/advanced/meta.json +9 -0
  31. package/docs/cookbook/advanced/publishing-libraries.mdx +336 -0
  32. package/docs/cookbook/advanced/serializable-steps.mdx +147 -0
  33. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
  34. package/docs/cookbook/agent-patterns/durable-agent.mdx +150 -0
  35. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +255 -0
  36. package/docs/cookbook/agent-patterns/meta.json +4 -0
  37. package/docs/cookbook/common-patterns/batching.mdx +105 -0
  38. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  39. package/docs/cookbook/common-patterns/meta.json +15 -0
  40. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  41. package/docs/cookbook/common-patterns/saga.mdx +247 -0
  42. package/docs/cookbook/common-patterns/scheduling.mdx +125 -0
  43. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  44. package/docs/cookbook/common-patterns/timeouts.mdx +99 -0
  45. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  46. package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
  47. package/docs/cookbook/index.mdx +38 -0
  48. package/docs/cookbook/integrations/ai-sdk.mdx +360 -0
  49. package/docs/cookbook/integrations/chat-sdk.mdx +303 -0
  50. package/docs/cookbook/integrations/meta.json +4 -0
  51. package/docs/cookbook/integrations/sandbox.mdx +516 -0
  52. package/docs/cookbook/meta.json +5 -0
  53. package/docs/deploying/world/local-world.mdx +1 -1
  54. package/docs/deploying/world/postgres-world.mdx +1 -1
  55. package/docs/deploying/world/vercel-world.mdx +1 -1
  56. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  57. package/docs/foundations/index.mdx +0 -3
  58. package/docs/foundations/meta.json +0 -1
  59. package/docs/foundations/serialization.mdx +1 -1
  60. package/docs/foundations/starting-workflows.mdx +1 -1
  61. package/docs/getting-started/index.mdx +8 -1
  62. package/docs/getting-started/meta.json +2 -1
  63. package/docs/getting-started/python.mdx +165 -0
  64. package/docs/meta.json +1 -0
  65. package/docs/migration-guides/index.mdx +34 -0
  66. package/docs/migration-guides/meta.json +9 -0
  67. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +363 -0
  68. package/docs/migration-guides/migrating-from-inngest.mdx +314 -0
  69. package/docs/migration-guides/migrating-from-temporal.mdx +318 -0
  70. package/docs/migration-guides/migrating-from-trigger-dev.mdx +337 -0
  71. package/package.json +13 -13
  72. package/docs/foundations/common-patterns.mdx +0 -265
@@ -0,0 +1,105 @@
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
+ - Bulk data imports (contacts, orders, products from a CSV)
13
+ - Processing hundreds or thousands of items against external APIs
14
+ - Calling rate-limited APIs where you need to control concurrency
15
+ - Any fan-out where you want failure isolation between groups
16
+
17
+ ## How it works
18
+
19
+ 1. Records are split into fixed-size batches.
20
+ 2. Each batch runs in parallel via `Promise.allSettled` — failures in one record don't affect others.
21
+ 3. A `sleep()` between batches paces requests to avoid overloading downstream services.
22
+ 4. After all batches, a summary is returned with succeeded/failed counts.
23
+
24
+ ## Pattern
25
+
26
+ The workflow splits records into chunks, processes each chunk concurrently, tracks results per batch, and returns a final tally.
27
+
28
+ ```typescript
29
+ import { sleep } from "workflow";
30
+
31
+ type Record = { name: string; email: string; role: string };
32
+
33
+ declare function processRecord(record: Record): Promise<string>; // @setup
34
+
35
+ export async function batchImport(records: Record[], batchSize: number) {
36
+ "use workflow";
37
+
38
+ let totalSucceeded = 0;
39
+ let totalFailed = 0;
40
+
41
+ for (let i = 0; i < records.length; i += batchSize) {
42
+ const batch = records.slice(i, i + batchSize);
43
+
44
+ // Run batch in parallel — failures are isolated per record
45
+ const outcomes = await Promise.allSettled( // [!code highlight]
46
+ batch.map((record) => processRecord(record))
47
+ );
48
+
49
+ for (let j = 0; j < outcomes.length; j++) {
50
+ if (outcomes[j].status === "fulfilled") {
51
+ totalSucceeded++;
52
+ } else {
53
+ totalFailed++;
54
+ }
55
+ }
56
+
57
+ // Pace between batches to avoid overloading downstream
58
+ if (i + batchSize < records.length) {
59
+ await sleep("1s"); // [!code highlight]
60
+ }
61
+ }
62
+
63
+ return { total: records.length, succeeded: totalSucceeded, failed: totalFailed };
64
+ }
65
+ ```
66
+
67
+ ### Step function
68
+
69
+ Each record is processed in its own step with full Node.js access and automatic retries.
70
+
71
+ ```typescript
72
+ type Record = { name: string; email: string; role: string };
73
+
74
+ async function processRecord(record: Record): Promise<string> {
75
+ "use step";
76
+ const res = await fetch(`https://api.example.com/contacts`, {
77
+ method: "POST",
78
+ body: JSON.stringify(record),
79
+ });
80
+ if (!res.ok) throw new Error(`Failed to import ${record.email}`);
81
+ const { id } = await res.json();
82
+ return id;
83
+ }
84
+ ```
85
+
86
+ ## Adapting to your use case
87
+
88
+ - Replace the `Record` type with your actual data shape (orders, images, products, etc.).
89
+ - Replace `processRecord()` with your real import logic — DB upserts, API calls, file processing.
90
+ - Tune `batchSize` and the `sleep()` duration to match your downstream rate limits.
91
+ - Add or remove tracking as needed — the pattern works with any item type.
92
+
93
+ ## Tips
94
+
95
+ - **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.
96
+ - **Tune batch size to your downstream API limits.** If the API allows 10 concurrent requests, use `batchSize: 10`.
97
+ - **Add pacing with `sleep()`** between batches to respect rate limits. The sleep is durable — it survives cold starts.
98
+ - **Each `processRecord` call is an independent step.** If one fails, it retries up to 3 times without affecting other items in the batch.
99
+
100
+ ## Key APIs
101
+
102
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
103
+ - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
104
+ - [`sleep()`](/docs/api-reference/workflow/sleep) -- pacing delay between batches
105
+ - [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) -- runs items in parallel, isolating failures
@@ -0,0 +1,107 @@
1
+ ---
2
+ title: Idempotency
3
+ description: Ensure external side effects happen exactly once, even when steps are retried or workflows are replayed.
4
+ type: guide
5
+ summary: Use step IDs as idempotency keys for external APIs like Stripe so that retries and replays don't create duplicate charges.
6
+ ---
7
+
8
+ Workflow steps can be retried (on failure) and replayed (on cold start). If a step calls an external API that isn't idempotent, retries could create duplicate charges, send duplicate emails, or double-process records. Use idempotency keys to make these operations safe.
9
+
10
+ ## When to use this
11
+
12
+ - Charging a payment (Stripe, PayPal)
13
+ - Sending transactional emails or SMS
14
+ - Creating records in external systems where duplicates are harmful
15
+ - Any step that has side effects in systems you don't control
16
+
17
+ ## Pattern: Step ID as idempotency key
18
+
19
+ Every step has a unique, deterministic `stepId` available via `getStepMetadata()`. Pass this as the idempotency key to external APIs:
20
+
21
+ ```typescript
22
+ import { getStepMetadata } from "workflow";
23
+
24
+ declare function createCharge(customerId: string, amount: number): Promise<{ id: string }>; // @setup
25
+ declare function sendReceipt(customerId: string, chargeId: string): Promise<void>; // @setup
26
+
27
+ export async function chargeCustomer(customerId: string, amount: number) {
28
+ "use workflow";
29
+
30
+ const charge = await createCharge(customerId, amount);
31
+ await sendReceipt(customerId, charge.id);
32
+
33
+ return { customerId, chargeId: charge.id, status: "completed" };
34
+ }
35
+ ```
36
+
37
+ ### Step function with idempotency key
38
+
39
+ ```typescript
40
+ import { getStepMetadata } from "workflow";
41
+
42
+ async function createCharge(
43
+ customerId: string,
44
+ amount: number
45
+ ): Promise<{ id: string }> {
46
+ "use step";
47
+
48
+ const { stepId } = getStepMetadata(); // [!code highlight]
49
+
50
+ // Stripe uses the idempotency key to deduplicate requests.
51
+ // If this step is retried, Stripe returns the same charge.
52
+ const charge = await fetch("https://api.stripe.com/v1/charges", {
53
+ method: "POST",
54
+ headers: {
55
+ Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
56
+ "Idempotency-Key": stepId, // [!code highlight]
57
+ },
58
+ body: new URLSearchParams({
59
+ amount: String(amount),
60
+ currency: "usd",
61
+ customer: customerId,
62
+ }),
63
+ });
64
+
65
+ if (!charge.ok) {
66
+ const error = await charge.json();
67
+ throw new Error(`Charge failed: ${error.message}`);
68
+ }
69
+
70
+ return charge.json();
71
+ }
72
+
73
+ async function sendReceipt(customerId: string, chargeId: string): Promise<void> {
74
+ "use step";
75
+
76
+ const { stepId } = getStepMetadata();
77
+
78
+ await fetch("https://api.example.com/receipts", {
79
+ method: "POST",
80
+ headers: { "Idempotency-Key": stepId },
81
+ body: JSON.stringify({ customerId, chargeId }),
82
+ });
83
+ }
84
+ ```
85
+
86
+ ## Race condition caveats
87
+
88
+ Workflow does not currently provide distributed locking or true exactly-once delivery across concurrent runs. If two workflow runs could process the same entity concurrently:
89
+
90
+ - **Rely on the external API's idempotency** (like Stripe's `Idempotency-Key`) rather than checking a local flag.
91
+ - **Don't use check-then-act patterns** like "read a flag, then write if not set" -- another run could read the same flag between your read and write.
92
+
93
+ If your external API doesn't support idempotency keys natively, consider adding a deduplication layer (e.g., a database unique constraint on the operation ID).
94
+
95
+ ## Tips
96
+
97
+ - **`stepId` is deterministic.** It's the same value across retries and replays of the same step, making it a reliable idempotency key.
98
+ - **Always provide idempotency keys for non-idempotent external calls.** Even if you think a step won't be retried, cold-start replay will re-execute it.
99
+ - **Handle 409/conflict as success.** If an external API returns "already processed," treat that as a successful result, not an error.
100
+ - **Make your own APIs idempotent** where possible. Accept an idempotency key and return the cached result on duplicate requests.
101
+
102
+ ## Key APIs
103
+
104
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- declares the orchestrator function
105
+ - [`"use step"`](/docs/api-reference/workflow/use-step) -- declares step functions with full Node.js access
106
+ - [`getStepMetadata()`](/docs/api-reference/step/get-step-metadata) -- provides the deterministic `stepId` for idempotency keys
107
+ - [`start()`](/docs/api-reference/workflow-api/start) -- starts a new workflow run
@@ -0,0 +1,15 @@
1
+ {
2
+ "title": "Common Patterns",
3
+ "defaultOpen": true,
4
+ "pages": [
5
+ "sequential-and-parallel",
6
+ "workflow-composition",
7
+ "saga",
8
+ "batching",
9
+ "rate-limiting",
10
+ "scheduling",
11
+ "timeouts",
12
+ "idempotency",
13
+ "webhooks"
14
+ ]
15
+ }
@@ -0,0 +1,228 @@
1
+ ---
2
+ title: Rate Limiting & Retries
3
+ description: Handle 429 responses and transient failures with RetryableError and exponential backoff.
4
+ type: guide
5
+ summary: When an external API returns 429, throw RetryableError with the Retry-After value so the workflow runtime automatically reschedules the step after the specified delay.
6
+ ---
7
+
8
+ Use this pattern when calling external APIs that enforce rate limits. Instead of writing manual retry loops, throw `RetryableError` with a `retryAfter` value and let the workflow runtime handle rescheduling.
9
+
10
+ ## When to use this
11
+
12
+ - Calling APIs that return 429 (Too Many Requests) with `Retry-After` headers
13
+ - Any step that hits transient failures and needs backoff
14
+ - Syncing data with third-party services (Stripe, CRMs, scrapers)
15
+
16
+ ## Pattern: RetryableError with Retry-After
17
+
18
+ A step function calls an external API. On 429, it reads the `Retry-After` header and throws `RetryableError`. The runtime reschedules the step automatically.
19
+
20
+ ```typescript
21
+ import { RetryableError } from "workflow";
22
+
23
+ declare function fetchFromCrm(contactId: string): Promise<unknown>; // @setup
24
+ declare function upsertToWarehouse(contactId: string, contact: unknown): Promise<void>; // @setup
25
+
26
+ export async function syncContact(contactId: string) {
27
+ "use workflow";
28
+
29
+ const contact = await fetchFromCrm(contactId);
30
+ await upsertToWarehouse(contactId, contact);
31
+
32
+ return { contactId, status: "synced" };
33
+ }
34
+ ```
35
+
36
+ ### Step function with rate limit handling
37
+
38
+ ```typescript
39
+ import { RetryableError } from "workflow";
40
+
41
+ async function fetchFromCrm(contactId: string) {
42
+ "use step";
43
+
44
+ const res = await fetch(`https://crm.example.com/contacts/${contactId}`);
45
+
46
+ if (res.status === 429) { // [!code highlight]
47
+ const retryAfter = res.headers.get("Retry-After");
48
+ throw new RetryableError("Rate limited by CRM", { // [!code highlight]
49
+ retryAfter: retryAfter ? parseInt(retryAfter) * 1000 : "1m",
50
+ });
51
+ }
52
+
53
+ if (!res.ok) throw new Error(`CRM returned ${res.status}`);
54
+ return res.json();
55
+ }
56
+
57
+ async function upsertToWarehouse(contactId: string, contact: unknown) {
58
+ "use step";
59
+ await fetch(`https://warehouse.example.com/contacts/${contactId}`, {
60
+ method: "PUT",
61
+ body: JSON.stringify(contact),
62
+ });
63
+ }
64
+ ```
65
+
66
+ ## Pattern: Exponential backoff
67
+
68
+ Use `getStepMetadata()` to access the current attempt number and calculate increasing delays:
69
+
70
+ ```typescript
71
+ import { RetryableError, getStepMetadata } from "workflow";
72
+
73
+ async function callFlakeyApi(endpoint: string) {
74
+ "use step";
75
+
76
+ const { attempt } = getStepMetadata(); // [!code highlight]
77
+ const res = await fetch(endpoint);
78
+
79
+ if (res.status === 429 || res.status >= 500) {
80
+ throw new RetryableError(`Request failed (${res.status})`, { // [!code highlight]
81
+ retryAfter: (attempt ** 2) * 1000, // 1s, 4s, 9s... // [!code highlight]
82
+ });
83
+ }
84
+
85
+ return res.json();
86
+ }
87
+ ```
88
+
89
+ ## Pattern: Circuit breaker with sleep
90
+
91
+ When a dependency is completely down, stop hitting it for a cooldown period using `sleep()`, then probe with a single test request:
92
+
93
+ ```typescript
94
+ import { sleep } from "workflow";
95
+
96
+ export async function circuitBreaker(maxRequests: number = 10) {
97
+ "use workflow";
98
+
99
+ let state: "closed" | "open" | "half-open" = "closed";
100
+ let consecutiveFailures = 0;
101
+ const FAILURE_THRESHOLD = 3;
102
+
103
+ for (let i = 1; i <= maxRequests; i++) {
104
+ if (state === "open") {
105
+ await sleep("30s"); // Durable cooldown // [!code highlight]
106
+ state = "half-open";
107
+ }
108
+
109
+ const success = await callService(i);
110
+
111
+ if (success) {
112
+ consecutiveFailures = 0;
113
+ if (state === "half-open") state = "closed";
114
+ } else {
115
+ consecutiveFailures++;
116
+ if (consecutiveFailures >= FAILURE_THRESHOLD) {
117
+ state = "open";
118
+ consecutiveFailures = 0;
119
+ }
120
+ }
121
+ }
122
+
123
+ return { status: state === "closed" ? "recovered" : "failed" };
124
+ }
125
+
126
+ async function callService(requestNum: number): Promise<boolean> {
127
+ "use step";
128
+ try {
129
+ const res = await fetch("https://payment-gateway.example.com/charge");
130
+ return res.ok;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Pattern: Custom max retries
138
+
139
+ Override the default retry count (3) for steps that need more or fewer attempts:
140
+
141
+ ```typescript
142
+ async function fetchWithRetries(url: string) {
143
+ "use step";
144
+ const res = await fetch(url);
145
+ if (!res.ok) throw new Error(`Failed: ${res.status}`);
146
+ return res.json();
147
+ }
148
+
149
+ // Allow up to 10 retry attempts
150
+ fetchWithRetries.maxRetries = 10; // [!code highlight]
151
+ ```
152
+
153
+ ## Application-level retry
154
+
155
+ Sometimes you need retry logic at the workflow level -- wrapping a step call with your own backoff instead of relying on the framework's built-in `RetryableError`. This is useful when you want full control over retry conditions, delays, and error filtering.
156
+
157
+ ```typescript
158
+ interface RetryOptions {
159
+ maxRetries?: number;
160
+ baseDelay?: number;
161
+ maxDelay?: number;
162
+ shouldRetry?: (error: Error, attempt: number) => boolean;
163
+ }
164
+
165
+ async function withRetry<T>(
166
+ fn: () => Promise<T>,
167
+ options: RetryOptions = {},
168
+ ): Promise<T> {
169
+ const { maxRetries = 3, baseDelay = 2000, maxDelay = 10000, shouldRetry } = options;
170
+ let lastError: Error | undefined;
171
+
172
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
173
+ try {
174
+ return await fn();
175
+ } catch (error) {
176
+ lastError = error instanceof Error ? error : new Error(String(error));
177
+ const isLastAttempt = attempt === maxRetries;
178
+ if (isLastAttempt || (shouldRetry && !shouldRetry(lastError, attempt + 1))) {
179
+ throw lastError;
180
+ }
181
+ // Exponential backoff with jitter
182
+ const delay = Math.min(baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5), maxDelay);
183
+ await new Promise(resolve => setTimeout(resolve, delay));
184
+ }
185
+ }
186
+
187
+ throw lastError;
188
+ }
189
+ ```
190
+
191
+ Use it in a workflow to wrap step calls:
192
+
193
+ ```typescript
194
+ declare function withRetry<T>(fn: () => Promise<T>, options?: { maxRetries?: number; shouldRetry?: (error: Error) => boolean }): Promise<T>; // @setup
195
+ declare function downloadFile(url: string): Promise<any>; // @setup
196
+
197
+ export async function downloadWithRetry(url: string) {
198
+ "use workflow";
199
+
200
+ const result = await withRetry(() => downloadFile(url), { // [!code highlight]
201
+ maxRetries: 5,
202
+ shouldRetry: (error) => error.message.includes("Timeout"),
203
+ });
204
+
205
+ return result;
206
+ }
207
+ ```
208
+
209
+ **When to use this vs `RetryableError`/`FatalError`:**
210
+ - **`RetryableError`** runs inside a step -- the framework reschedules the step after the delay. Use it for transient HTTP errors (429, 503) where the runtime should handle backoff.
211
+ - **Application-level retry** wraps the step call from the workflow. Use it when you need custom retry conditions, want to retry across different steps, or when you're building a library and prefer not to depend on workflow-specific error classes.
212
+
213
+ ## Tips
214
+
215
+ - **`RetryableError` is for transient failures.** Use it when the request might succeed on a later attempt (429, 503, network timeout).
216
+ - **`FatalError` is for permanent failures.** Use it when retrying won't help (404, 401, invalid input). This skips all remaining retries.
217
+ - **The `retryAfter` option accepts** a millisecond number, a duration string (`"1m"`, `"30s"`), or a `Date` object.
218
+ - **Steps retry up to 3 times by default.** Set `fn.maxRetries = N` to change this per step function.
219
+ - **Don't write manual sleep-retry loops.** The runtime handles scheduling natively with `RetryableError` -- it's more efficient and survives cold starts.
220
+
221
+ ## Key APIs
222
+
223
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
224
+ - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
225
+ - [`RetryableError`](/docs/api-reference/workflow/retryable-error) -- signals the runtime to retry after a delay
226
+ - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- signals a permanent failure, skipping retries
227
+ - [`getStepMetadata()`](/docs/api-reference/step/get-step-metadata) -- provides the current attempt number and step ID
228
+ - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable pause for circuit breaker cooldowns