@workflow/core 4.0.1-beta.9 → 4.1.0-beta.52

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 (184) hide show
  1. package/dist/builtins.js +1 -1
  2. package/dist/class-serialization.d.ts +26 -0
  3. package/dist/class-serialization.d.ts.map +1 -0
  4. package/dist/class-serialization.js +66 -0
  5. package/dist/create-hook.js +1 -1
  6. package/dist/define-hook.d.ts +40 -25
  7. package/dist/define-hook.d.ts.map +1 -1
  8. package/dist/define-hook.js +22 -27
  9. package/dist/events-consumer.d.ts.map +1 -1
  10. package/dist/events-consumer.js +5 -1
  11. package/dist/flushable-stream.d.ts +82 -0
  12. package/dist/flushable-stream.d.ts.map +1 -0
  13. package/dist/flushable-stream.js +214 -0
  14. package/dist/global.d.ts +4 -1
  15. package/dist/global.d.ts.map +1 -1
  16. package/dist/global.js +21 -9
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/logger.js +1 -1
  21. package/dist/observability.d.ts +60 -0
  22. package/dist/observability.d.ts.map +1 -1
  23. package/dist/observability.js +265 -32
  24. package/dist/private.d.ts +10 -1
  25. package/dist/private.d.ts.map +1 -1
  26. package/dist/private.js +6 -1
  27. package/dist/runtime/helpers.d.ts +52 -0
  28. package/dist/runtime/helpers.d.ts.map +1 -0
  29. package/dist/runtime/helpers.js +264 -0
  30. package/dist/runtime/resume-hook.d.ts +17 -12
  31. package/dist/runtime/resume-hook.d.ts.map +1 -1
  32. package/dist/runtime/resume-hook.js +79 -64
  33. package/dist/runtime/run.d.ts +100 -0
  34. package/dist/runtime/run.d.ts.map +1 -0
  35. package/dist/runtime/run.js +132 -0
  36. package/dist/runtime/start.d.ts +15 -1
  37. package/dist/runtime/start.d.ts.map +1 -1
  38. package/dist/runtime/start.js +72 -46
  39. package/dist/runtime/step-handler.d.ts +7 -0
  40. package/dist/runtime/step-handler.d.ts.map +1 -0
  41. package/dist/runtime/step-handler.js +337 -0
  42. package/dist/runtime/suspension-handler.d.ts +25 -0
  43. package/dist/runtime/suspension-handler.d.ts.map +1 -0
  44. package/dist/runtime/suspension-handler.js +182 -0
  45. package/dist/runtime/world.d.ts.map +1 -1
  46. package/dist/runtime/world.js +20 -21
  47. package/dist/runtime.d.ts +4 -105
  48. package/dist/runtime.d.ts.map +1 -1
  49. package/dist/runtime.js +97 -531
  50. package/dist/schemas.d.ts +1 -15
  51. package/dist/schemas.d.ts.map +1 -1
  52. package/dist/schemas.js +2 -15
  53. package/dist/serialization.d.ts +112 -21
  54. package/dist/serialization.d.ts.map +1 -1
  55. package/dist/serialization.js +469 -85
  56. package/dist/sleep.d.ts +10 -0
  57. package/dist/sleep.d.ts.map +1 -1
  58. package/dist/sleep.js +1 -1
  59. package/dist/source-map.d.ts +10 -0
  60. package/dist/source-map.d.ts.map +1 -0
  61. package/dist/source-map.js +56 -0
  62. package/dist/step/context-storage.d.ts +2 -1
  63. package/dist/step/context-storage.d.ts.map +1 -1
  64. package/dist/step/context-storage.js +1 -1
  65. package/dist/step/get-closure-vars.d.ts +9 -0
  66. package/dist/step/get-closure-vars.d.ts.map +1 -0
  67. package/dist/step/get-closure-vars.js +16 -0
  68. package/dist/step/get-step-metadata.js +1 -1
  69. package/dist/step/get-workflow-metadata.js +1 -1
  70. package/dist/step/writable-stream.d.ts +10 -2
  71. package/dist/step/writable-stream.d.ts.map +1 -1
  72. package/dist/step/writable-stream.js +6 -5
  73. package/dist/step.d.ts +1 -1
  74. package/dist/step.d.ts.map +1 -1
  75. package/dist/step.js +93 -47
  76. package/dist/symbols.d.ts +6 -0
  77. package/dist/symbols.d.ts.map +1 -1
  78. package/dist/symbols.js +7 -1
  79. package/dist/telemetry/semantic-conventions.d.ts +66 -38
  80. package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
  81. package/dist/telemetry/semantic-conventions.js +16 -3
  82. package/dist/telemetry.d.ts +8 -4
  83. package/dist/telemetry.d.ts.map +1 -1
  84. package/dist/telemetry.js +39 -6
  85. package/dist/types.js +1 -1
  86. package/dist/util.d.ts +5 -24
  87. package/dist/util.d.ts.map +1 -1
  88. package/dist/util.js +19 -38
  89. package/dist/version.d.ts +2 -0
  90. package/dist/version.d.ts.map +1 -0
  91. package/dist/version.js +3 -0
  92. package/dist/vm/index.js +2 -2
  93. package/dist/vm/uuid.js +1 -1
  94. package/dist/workflow/create-hook.js +1 -1
  95. package/dist/workflow/define-hook.d.ts +3 -3
  96. package/dist/workflow/define-hook.d.ts.map +1 -1
  97. package/dist/workflow/define-hook.js +1 -1
  98. package/dist/workflow/get-workflow-metadata.js +1 -1
  99. package/dist/workflow/hook.d.ts.map +1 -1
  100. package/dist/workflow/hook.js +49 -14
  101. package/dist/workflow/index.d.ts +1 -1
  102. package/dist/workflow/index.d.ts.map +1 -1
  103. package/dist/workflow/index.js +2 -2
  104. package/dist/workflow/sleep.d.ts +1 -1
  105. package/dist/workflow/sleep.d.ts.map +1 -1
  106. package/dist/workflow/sleep.js +26 -39
  107. package/dist/workflow/writable-stream.d.ts +1 -1
  108. package/dist/workflow/writable-stream.d.ts.map +1 -1
  109. package/dist/workflow/writable-stream.js +1 -1
  110. package/dist/workflow.d.ts +1 -1
  111. package/dist/workflow.d.ts.map +1 -1
  112. package/dist/workflow.js +72 -9
  113. package/docs/api-reference/create-hook.mdx +134 -0
  114. package/docs/api-reference/create-webhook.mdx +226 -0
  115. package/docs/api-reference/define-hook.mdx +207 -0
  116. package/docs/api-reference/fatal-error.mdx +38 -0
  117. package/docs/api-reference/fetch.mdx +140 -0
  118. package/docs/api-reference/get-step-metadata.mdx +77 -0
  119. package/docs/api-reference/get-workflow-metadata.mdx +45 -0
  120. package/docs/api-reference/get-writable.mdx +292 -0
  121. package/docs/api-reference/index.mdx +56 -0
  122. package/docs/api-reference/meta.json +3 -0
  123. package/docs/api-reference/retryable-error.mdx +107 -0
  124. package/docs/api-reference/sleep.mdx +60 -0
  125. package/docs/foundations/common-patterns.mdx +254 -0
  126. package/docs/foundations/errors-and-retries.mdx +191 -0
  127. package/docs/foundations/hooks.mdx +456 -0
  128. package/docs/foundations/idempotency.mdx +56 -0
  129. package/docs/foundations/index.mdx +33 -0
  130. package/docs/foundations/meta.json +14 -0
  131. package/docs/foundations/serialization.mdx +158 -0
  132. package/docs/foundations/starting-workflows.mdx +212 -0
  133. package/docs/foundations/streaming.mdx +570 -0
  134. package/docs/foundations/workflows-and-steps.mdx +198 -0
  135. package/docs/how-it-works/code-transform.mdx +335 -0
  136. package/docs/how-it-works/event-sourcing.mdx +255 -0
  137. package/docs/how-it-works/framework-integrations.mdx +438 -0
  138. package/docs/how-it-works/meta.json +10 -0
  139. package/docs/how-it-works/understanding-directives.mdx +612 -0
  140. package/package.json +31 -25
  141. package/dist/builtins.js.map +0 -1
  142. package/dist/create-hook.js.map +0 -1
  143. package/dist/define-hook.js.map +0 -1
  144. package/dist/events-consumer.js.map +0 -1
  145. package/dist/global.js.map +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/logger.js.map +0 -1
  148. package/dist/observability.js.map +0 -1
  149. package/dist/parse-name.d.ts +0 -25
  150. package/dist/parse-name.d.ts.map +0 -1
  151. package/dist/parse-name.js +0 -40
  152. package/dist/parse-name.js.map +0 -1
  153. package/dist/private.js.map +0 -1
  154. package/dist/runtime/resume-hook.js.map +0 -1
  155. package/dist/runtime/start.js.map +0 -1
  156. package/dist/runtime/world.js.map +0 -1
  157. package/dist/runtime.js.map +0 -1
  158. package/dist/schemas.js.map +0 -1
  159. package/dist/serialization.js.map +0 -1
  160. package/dist/sleep.js.map +0 -1
  161. package/dist/step/context-storage.js.map +0 -1
  162. package/dist/step/get-step-metadata.js.map +0 -1
  163. package/dist/step/get-workflow-metadata.js.map +0 -1
  164. package/dist/step/writable-stream.js.map +0 -1
  165. package/dist/step.js.map +0 -1
  166. package/dist/symbols.js.map +0 -1
  167. package/dist/telemetry/semantic-conventions.js.map +0 -1
  168. package/dist/telemetry.js.map +0 -1
  169. package/dist/types.js.map +0 -1
  170. package/dist/util.js.map +0 -1
  171. package/dist/vm/index.js.map +0 -1
  172. package/dist/vm/uuid.js.map +0 -1
  173. package/dist/workflow/create-hook.js.map +0 -1
  174. package/dist/workflow/define-hook.js.map +0 -1
  175. package/dist/workflow/get-workflow-metadata.js.map +0 -1
  176. package/dist/workflow/hook.js.map +0 -1
  177. package/dist/workflow/index.js.map +0 -1
  178. package/dist/workflow/sleep.js.map +0 -1
  179. package/dist/workflow/writable-stream.js.map +0 -1
  180. package/dist/workflow.js.map +0 -1
  181. package/dist/writable-stream.d.ts +0 -23
  182. package/dist/writable-stream.d.ts.map +0 -1
  183. package/dist/writable-stream.js +0 -17
  184. package/dist/writable-stream.js.map +0 -1
@@ -0,0 +1,254 @@
1
+ ---
2
+ title: Common Patterns
3
+ description: Implement distributed patterns using familiar async/await syntax with no new APIs to learn.
4
+ ---
5
+
6
+ Common distributed patterns are simple to implement in workflows and require learning no new syntax. You can just use familiar async/await patterns.
7
+
8
+ ## Sequential Execution
9
+
10
+ The simplest way to orchestrate steps is to execute them one after another, where each step can be dependent on the previous step.
11
+
12
+ ```typescript lineNumbers
13
+ declare function validateData(data: unknown): Promise<string>; // @setup
14
+ declare function processData(data: string): Promise<string>; // @setup
15
+ declare function storeData(data: string): Promise<string>; // @setup
16
+
17
+ export async function dataPipelineWorkflow(data: unknown) {
18
+ "use workflow";
19
+
20
+ const validated = await validateData(data);
21
+ const processed = await processData(validated);
22
+ const stored = await storeData(processed);
23
+
24
+ return stored;
25
+ }
26
+ ```
27
+
28
+ ## Parallel Execution
29
+
30
+ When you need to execute multiple steps in parallel, you can use `Promise.all` to run them all at the same time.
31
+
32
+ ```typescript lineNumbers
33
+ declare function fetchUser(userId: string): Promise<{ name: string }>; // @setup
34
+ declare function fetchOrders(userId: string): Promise<{ items: string[] }>; // @setup
35
+ declare function fetchPreferences(userId: string): Promise<{ theme: string }>; // @setup
36
+
37
+ export async function fetchUserData(userId: string) {
38
+ "use workflow";
39
+
40
+ const [user, orders, preferences] = await Promise.all([ // [!code highlight]
41
+ fetchUser(userId), // [!code highlight]
42
+ fetchOrders(userId), // [!code highlight]
43
+ fetchPreferences(userId) // [!code highlight]
44
+ ]); // [!code highlight]
45
+
46
+ return { user, orders, preferences };
47
+ }
48
+ ```
49
+
50
+ This not only applies to steps - since [`sleep()`](/docs/api-reference/workflow/sleep) and [`webhook`](/docs/api-reference/workflow/create-webhook) are also just promises, we can await those in parallel too.
51
+ We can also use `Promise.race` instead of `Promise.all` to stop executing promises after the first one completes.
52
+
53
+ ```typescript lineNumbers
54
+ import { sleep, createWebhook } from "workflow";
55
+ declare function executeExternalTask(webhookUrl: string): Promise<void>; // @setup
56
+
57
+ export async function runExternalTask(userId: string) {
58
+ "use workflow";
59
+
60
+ const webhook = createWebhook();
61
+ await executeExternalTask(webhook.url); // Send the webhook somewhere
62
+
63
+ // Wait for the external webhook to be hit, with a timeout of 1 day,
64
+ // whichever comes first
65
+ await Promise.race([ // [!code highlight]
66
+ webhook, // [!code highlight]
67
+ sleep("1 day"), // [!code highlight]
68
+ ]); // [!code highlight]
69
+
70
+ console.log("Done")
71
+ }
72
+ ```
73
+
74
+ ## A Full Example
75
+
76
+ Here's a simplified example taken from the [birthday card generator demo](https://github.com/vercel/workflow-examples/tree/main/birthday-card-generator), to illustrate how sequential and parallel execution can be combined.
77
+
78
+ ```typescript lineNumbers
79
+ import { createWebhook, sleep, type Webhook } from "workflow"
80
+ declare function makeCardText(prompt: string): Promise<string>; // @setup
81
+ declare function makeCardImage(text: string): Promise<string>; // @setup
82
+ declare function sendRSVPEmail(friend: string, webhook: Webhook): Promise<void>; // @setup
83
+ declare function sendBirthdayCard(text: string, image: string, rsvps: unknown[], email: string): Promise<void>; // @setup
84
+
85
+ async function birthdayWorkflow(
86
+ prompt: string,
87
+ email: string,
88
+ friends: string[],
89
+ birthday: Date
90
+ ) {
91
+ "use workflow";
92
+
93
+ // Generate a birthday card with sequential steps
94
+ const text = await makeCardText(prompt)
95
+ const image = await makeCardImage(text)
96
+
97
+ // Create webhooks for each friend who's invited to the birthday party
98
+ const webhooks = friends.map(_ => createWebhook())
99
+
100
+ // Send out all the RSVP invites in parallel steps
101
+ await Promise.all(
102
+ friends.map(
103
+ (friend, i) => sendRSVPEmail(friend, webhooks[i])
104
+ )
105
+ )
106
+
107
+ // Collect RSVPs as they are made without blocking the workflow
108
+ let rsvps = []
109
+ webhooks.map(
110
+ webhook => webhook
111
+ .then(req => req.json())
112
+ .then(( { rsvp } ) => rsvps.push(rsvp))
113
+ )
114
+
115
+ // Wait until the birthday
116
+ await sleep(birthday)
117
+
118
+ // Send birthday card with as many rsvps were collected
119
+ await sendBirthdayCard(text, image, rsvps, email)
120
+
121
+ return { text, image, status: "Sent" }
122
+ }
123
+ ```
124
+
125
+ ## Timeout Pattern
126
+
127
+ A common requirement is adding timeouts to operations that might take too long. Use `Promise.race` with `sleep()` to implement this pattern.
128
+
129
+ ```typescript lineNumbers
130
+ import { sleep } from "workflow";
131
+ declare function processData(data: string): Promise<string>; // @setup
132
+
133
+ export async function processWithTimeout(data: string) {
134
+ "use workflow";
135
+
136
+ const result = await Promise.race([ // [!code highlight]
137
+ processData(data), // [!code highlight]
138
+ sleep("30s").then(() => "timeout" as const), // [!code highlight]
139
+ ]); // [!code highlight]
140
+
141
+ if (result === "timeout") {
142
+ // In workflows, any thrown error exits the workflow (FatalError is for steps)
143
+ throw new Error("Processing timed out after 30 seconds");
144
+ }
145
+
146
+ return result;
147
+ }
148
+ ```
149
+
150
+ This pattern works with any promise-returning operation including steps, hooks, and webhooks. For example, you can add a timeout to a webhook that waits for external input:
151
+
152
+ ```typescript lineNumbers
153
+ import { sleep, createWebhook } from "workflow";
154
+ declare function sendApprovalRequest(requestId: string, webhookUrl: string): Promise<void>; // @setup
155
+
156
+ export async function waitForApproval(requestId: string) {
157
+ "use workflow";
158
+
159
+ const webhook = createWebhook<{ approved: boolean }>();
160
+ await sendApprovalRequest(requestId, webhook.url);
161
+
162
+ const result = await Promise.race([ // [!code highlight]
163
+ webhook.then((req) => req.json()), // [!code highlight]
164
+ sleep("7 days").then(() => ({ timedOut: true }) as const), // [!code highlight]
165
+ ]); // [!code highlight]
166
+
167
+ if ("timedOut" in result) {
168
+ throw new Error("Approval request expired after 7 days");
169
+ }
170
+
171
+ return result.approved;
172
+ }
173
+ ```
174
+
175
+ ## Workflow Composition
176
+
177
+ Workflows can call other workflows, enabling you to break complex processes into reusable building blocks. There are two approaches depending on your needs.
178
+
179
+ ### Direct Await (Flattening)
180
+
181
+ Call a child workflow directly using `await`. This "flattens" the child workflow into the parent - the child's steps execute inline within the parent workflow's context.
182
+
183
+ ```typescript lineNumbers
184
+ declare function sendEmail(userId: string): Promise<void>; // @setup
185
+ declare function sendPushNotification(userId: string): Promise<void>; // @setup
186
+ declare function createAccount(userId: string): Promise<void>; // @setup
187
+ declare function setupPreferences(userId: string): Promise<void>; // @setup
188
+
189
+ // Child workflow
190
+ export async function sendNotifications(userId: string) {
191
+ "use workflow";
192
+
193
+ await sendEmail(userId);
194
+ await sendPushNotification(userId);
195
+ return { notified: true };
196
+ }
197
+
198
+ // Parent workflow calls child directly
199
+ export async function onboardUser(userId: string) {
200
+ "use workflow";
201
+
202
+ await createAccount(userId);
203
+ await sendNotifications(userId); // [!code highlight]
204
+ await setupPreferences(userId);
205
+
206
+ return { userId, status: "onboarded" };
207
+ }
208
+ ```
209
+
210
+ With direct await, the parent workflow waits for the child to complete before continuing. The child's steps appear in the parent's event log as if they were called directly from the parent.
211
+
212
+ ### Background Execution via Step
213
+
214
+ To run a child workflow independently without blocking the parent, use a step that calls [`start()`](/docs/api-reference/workflow-api/start). This launches the child workflow in the background.
215
+
216
+ ```typescript lineNumbers
217
+ import { start } from "workflow/api";
218
+ declare function generateReport(reportId: string): Promise<void>; // @setup
219
+ declare function fulfillOrder(orderId: string): Promise<{ id: string }>; // @setup
220
+ declare function sendConfirmation(orderId: string): Promise<void>; // @setup
221
+
222
+ // Step that starts a workflow in the background
223
+ async function triggerReportGeneration(reportId: string) {
224
+ "use step";
225
+
226
+ const run = await start(generateReport, [reportId]); // [!code highlight]
227
+ return run.runId;
228
+ }
229
+
230
+ // Parent workflow
231
+ export async function processOrder(orderId: string) {
232
+ "use workflow";
233
+
234
+ const order = await fulfillOrder(orderId);
235
+
236
+ // Fire off report generation without waiting
237
+ const reportRunId = await triggerReportGeneration(orderId); // [!code highlight]
238
+
239
+ // Continue immediately - report generates in background
240
+ await sendConfirmation(orderId);
241
+
242
+ return { orderId, reportRunId };
243
+ }
244
+ ```
245
+
246
+ With background execution, the parent workflow continues immediately after starting the child. The child workflow runs independently with its own event log and can be monitored separately using the returned `runId`.
247
+
248
+ **Choose direct await when:**
249
+ - The parent needs the child's result before continuing
250
+ - You want a single, unified event log
251
+
252
+ **Choose background execution when:**
253
+ - The parent doesn't need to wait for the result
254
+ - You want separate workflow runs for observability
@@ -0,0 +1,191 @@
1
+ ---
2
+ title: Errors & Retrying
3
+ description: Customize retry behavior with FatalError and RetryableError for robust error handling.
4
+ ---
5
+
6
+ By default, errors thrown inside steps are retried. Additionally, Workflow DevKit provides two new types of errors you can use to customize retries.
7
+
8
+ ## Default Retrying
9
+
10
+ By default, steps retry up to 3 times on arbitrary errors. You can customize the number of retries by adding a `maxRetries` property to the step function.
11
+
12
+ ```typescript lineNumbers
13
+ async function callApi(endpoint: string) {
14
+ "use step";
15
+
16
+ const response = await fetch(endpoint);
17
+
18
+ if (response.status >= 500) {
19
+ // Any uncaught error gets retried
20
+ throw new Error("Uncaught exceptions get retried!"); // [!code highlight]
21
+ }
22
+
23
+ return response.json();
24
+ }
25
+
26
+ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
27
+ ```
28
+
29
+ Steps get enqueued immediately after a failure. Read on to see how this can be customized.
30
+
31
+ <Callout type="info">
32
+ When a retried step performs external side effects (payments, emails, API
33
+ writes), ensure those calls are <strong>idempotent</strong> to avoid duplicate
34
+ side effects. See <a href="/docs/foundations/idempotency">Idempotency</a> for
35
+ more information.
36
+ </Callout>
37
+
38
+ ## Intentional Errors
39
+
40
+ When your step needs to intentionally throw an error and skip retrying, simply throw a [`FatalError`](/docs/api-reference/workflow/fatal-error).
41
+
42
+ ```typescript lineNumbers
43
+ import { FatalError } from "workflow";
44
+
45
+ async function callApi(endpoint: string) {
46
+ "use step";
47
+
48
+ const response = await fetch(endpoint);
49
+
50
+ if (response.status >= 500) {
51
+ // Any uncaught error gets retried
52
+ throw new Error("Uncaught exceptions get retried!");
53
+ }
54
+
55
+ if (response.status === 404) {
56
+ throw new FatalError("Resource not found. Skipping retries."); // [!code highlight]
57
+ }
58
+
59
+ return response.json();
60
+ }
61
+ ```
62
+
63
+ ## Customize Retry Behavior
64
+
65
+ When you need to customize the delay on a retry, use [`RetryableError`](/docs/api-reference/workflow/retryable-error) and set the `retryAfter` property.
66
+
67
+ ```typescript lineNumbers
68
+ import { FatalError, RetryableError } from "workflow";
69
+
70
+ async function callApi(endpoint: string) {
71
+ "use step";
72
+
73
+ const response = await fetch(endpoint);
74
+
75
+ if (response.status >= 500) {
76
+ throw new Error("Uncaught exceptions get retried!");
77
+ }
78
+
79
+ if (response.status === 404) {
80
+ throw new FatalError("Resource not found. Skipping retries.");
81
+ }
82
+
83
+ if (response.status === 429) {
84
+ throw new RetryableError("Rate limited. Retrying...", { // [!code highlight]
85
+ retryAfter: "1m", // Duration string // [!code highlight]
86
+ }); // [!code highlight]
87
+ }
88
+
89
+ return response.json();
90
+ }
91
+ ```
92
+
93
+ ## Advanced Example
94
+
95
+ This final example combines everything we've learned, along with [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata).
96
+
97
+ ```typescript lineNumbers
98
+ import { FatalError, RetryableError, getStepMetadata } from "workflow";
99
+
100
+ async function callApi(endpoint: string) {
101
+ "use step";
102
+
103
+ const metadata = getStepMetadata();
104
+
105
+ const response = await fetch(endpoint);
106
+
107
+ if (response.status >= 500) {
108
+ // Exponential backoffs
109
+ throw new RetryableError("Backing off...", {
110
+ retryAfter: (metadata.attempt ** 2) * 1000, // [!code highlight]
111
+ });
112
+ }
113
+
114
+ if (response.status === 404) {
115
+ throw new FatalError("Resource not found. Skipping retries.");
116
+ }
117
+
118
+ if (response.status === 429) {
119
+ throw new RetryableError("Rate limited. Retrying...", {
120
+ retryAfter: new Date(Date.now() + 60000), // Date instance // [!code highlight]
121
+ });
122
+ }
123
+
124
+ return response.json();
125
+ }
126
+ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
127
+ ```
128
+
129
+ <Callout type="info">
130
+ Setting <code>maxRetries = 0</code> means the step will run once but will not
131
+ be retried on failure. The default is <code>maxRetries = 3</code>, meaning the
132
+ step can run up to 4 times total (1 initial attempt + 3 retries).
133
+ </Callout>
134
+
135
+ ## Rolling Back Failed Steps
136
+
137
+ When a workflow fails partway through, it can leave the system in an inconsistent state.
138
+ A common pattern to address this is "rollbacks": for each successful step, record a corresponding rollback action that can undo it.
139
+ If a later step fails, run the rollbacks in reverse order to roll back.
140
+
141
+ Key guidelines:
142
+
143
+ - Make rollbacks steps as well, so they are durable and benefit from retries.
144
+ - Ensure rollbacks are [idempotent](/docs/foundations/idempotency); they may run more than once.
145
+ - Only enqueue a compensation after its forward step succeeds.
146
+
147
+ ```typescript lineNumbers
148
+ // Forward steps
149
+ async function reserveInventory(orderId: string) {
150
+ "use step";
151
+ // ... call inventory service to reserve ...
152
+ }
153
+
154
+ async function chargePayment(orderId: string) {
155
+ "use step";
156
+ // ... charge the customer ...
157
+ }
158
+
159
+ // Rollback steps
160
+ async function releaseInventory(orderId: string) {
161
+ "use step";
162
+ // ... undo inventory reservation ...
163
+ }
164
+
165
+ async function refundPayment(orderId: string) {
166
+ "use step";
167
+ // ... refund the charge ...
168
+ }
169
+
170
+ export async function placeOrderSaga(orderId: string) {
171
+ "use workflow";
172
+
173
+ const rollbacks: Array<() => Promise<void>> = [];
174
+
175
+ try {
176
+ await reserveInventory(orderId);
177
+ rollbacks.push(() => releaseInventory(orderId));
178
+
179
+ await chargePayment(orderId);
180
+ rollbacks.push(() => refundPayment(orderId));
181
+
182
+ // ... more steps & rollbacks ...
183
+ } catch (e) {
184
+ for (const rollback of rollbacks.reverse()) {
185
+ await rollback();
186
+ }
187
+ // Rethrow so the workflow records the failure after rollbacks
188
+ throw e;
189
+ }
190
+ }
191
+ ```