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.
- package/README.md +4 -4
- 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/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 +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.js +1 -1
- 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 +34 -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/cookbook/advanced/child-workflows.mdx +372 -0
- package/docs/cookbook/advanced/distributed-abort-controller.mdx +318 -0
- package/docs/cookbook/advanced/meta.json +9 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +336 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +147 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +205 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +150 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +255 -0
- package/docs/cookbook/agent-patterns/meta.json +4 -0
- package/docs/cookbook/common-patterns/batching.mdx +105 -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 +247 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +125 -0
- 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/webhooks.mdx +185 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +118 -0
- package/docs/cookbook/index.mdx +38 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +360 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +303 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +516 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +1 -1
- package/docs/deploying/world/vercel-world.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +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/getting-started/index.mdx +8 -1
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/python.mdx +165 -0
- 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 +363 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +314 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +318 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +337 -0
- package/package.json +13 -13
- 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,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
|