@workflow/core 4.0.1-beta.8 → 4.1.0-beta.51

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 (179) 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 +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -3
  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/start.d.ts +14 -0
  34. package/dist/runtime/start.d.ts.map +1 -1
  35. package/dist/runtime/start.js +71 -45
  36. package/dist/runtime/step-handler.d.ts +7 -0
  37. package/dist/runtime/step-handler.d.ts.map +1 -0
  38. package/dist/runtime/step-handler.js +337 -0
  39. package/dist/runtime/suspension-handler.d.ts +25 -0
  40. package/dist/runtime/suspension-handler.d.ts.map +1 -0
  41. package/dist/runtime/suspension-handler.js +182 -0
  42. package/dist/runtime/world.d.ts.map +1 -1
  43. package/dist/runtime/world.js +20 -21
  44. package/dist/runtime.d.ts +3 -7
  45. package/dist/runtime.d.ts.map +1 -1
  46. package/dist/runtime.js +103 -410
  47. package/dist/schemas.d.ts +1 -15
  48. package/dist/schemas.d.ts.map +1 -1
  49. package/dist/schemas.js +2 -15
  50. package/dist/serialization.d.ts +112 -21
  51. package/dist/serialization.d.ts.map +1 -1
  52. package/dist/serialization.js +469 -85
  53. package/dist/sleep.d.ts +10 -0
  54. package/dist/sleep.d.ts.map +1 -1
  55. package/dist/sleep.js +1 -1
  56. package/dist/source-map.d.ts +10 -0
  57. package/dist/source-map.d.ts.map +1 -0
  58. package/dist/source-map.js +56 -0
  59. package/dist/step/context-storage.d.ts +2 -0
  60. package/dist/step/context-storage.d.ts.map +1 -1
  61. package/dist/step/context-storage.js +1 -1
  62. package/dist/step/get-closure-vars.d.ts +9 -0
  63. package/dist/step/get-closure-vars.d.ts.map +1 -0
  64. package/dist/step/get-closure-vars.js +16 -0
  65. package/dist/step/get-step-metadata.js +1 -1
  66. package/dist/step/get-workflow-metadata.js +1 -1
  67. package/dist/{writable-stream.d.ts → step/writable-stream.d.ts} +5 -5
  68. package/dist/step/writable-stream.d.ts.map +1 -0
  69. package/dist/step/writable-stream.js +30 -0
  70. package/dist/step.d.ts +1 -1
  71. package/dist/step.d.ts.map +1 -1
  72. package/dist/step.js +93 -47
  73. package/dist/symbols.d.ts +6 -0
  74. package/dist/symbols.d.ts.map +1 -1
  75. package/dist/symbols.js +7 -1
  76. package/dist/telemetry/semantic-conventions.d.ts +66 -38
  77. package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
  78. package/dist/telemetry/semantic-conventions.js +16 -3
  79. package/dist/telemetry.d.ts +8 -4
  80. package/dist/telemetry.d.ts.map +1 -1
  81. package/dist/telemetry.js +39 -6
  82. package/dist/types.js +1 -1
  83. package/dist/util.d.ts +5 -24
  84. package/dist/util.d.ts.map +1 -1
  85. package/dist/util.js +19 -38
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.d.ts.map +1 -0
  88. package/dist/version.js +3 -0
  89. package/dist/vm/index.js +2 -2
  90. package/dist/vm/uuid.js +1 -1
  91. package/dist/workflow/create-hook.js +1 -1
  92. package/dist/workflow/define-hook.d.ts +3 -3
  93. package/dist/workflow/define-hook.d.ts.map +1 -1
  94. package/dist/workflow/define-hook.js +1 -1
  95. package/dist/workflow/get-workflow-metadata.js +1 -1
  96. package/dist/workflow/hook.d.ts.map +1 -1
  97. package/dist/workflow/hook.js +49 -14
  98. package/dist/workflow/index.d.ts +1 -1
  99. package/dist/workflow/index.d.ts.map +1 -1
  100. package/dist/workflow/index.js +2 -2
  101. package/dist/workflow/sleep.d.ts +1 -1
  102. package/dist/workflow/sleep.d.ts.map +1 -1
  103. package/dist/workflow/sleep.js +26 -39
  104. package/dist/workflow/writable-stream.d.ts +1 -1
  105. package/dist/workflow/writable-stream.d.ts.map +1 -1
  106. package/dist/workflow/writable-stream.js +1 -1
  107. package/dist/workflow.d.ts +1 -1
  108. package/dist/workflow.d.ts.map +1 -1
  109. package/dist/workflow.js +72 -9
  110. package/docs/api-reference/create-hook.mdx +133 -0
  111. package/docs/api-reference/create-webhook.mdx +225 -0
  112. package/docs/api-reference/define-hook.mdx +206 -0
  113. package/docs/api-reference/fatal-error.mdx +37 -0
  114. package/docs/api-reference/fetch.mdx +139 -0
  115. package/docs/api-reference/get-step-metadata.mdx +76 -0
  116. package/docs/api-reference/get-workflow-metadata.mdx +44 -0
  117. package/docs/api-reference/get-writable.mdx +292 -0
  118. package/docs/api-reference/index.mdx +55 -0
  119. package/docs/api-reference/meta.json +3 -0
  120. package/docs/api-reference/retryable-error.mdx +106 -0
  121. package/docs/api-reference/sleep.mdx +59 -0
  122. package/docs/foundations/common-patterns.mdx +253 -0
  123. package/docs/foundations/errors-and-retries.mdx +190 -0
  124. package/docs/foundations/hooks.mdx +455 -0
  125. package/docs/foundations/idempotency.mdx +55 -0
  126. package/docs/foundations/index.mdx +32 -0
  127. package/docs/foundations/meta.json +14 -0
  128. package/docs/foundations/serialization.mdx +157 -0
  129. package/docs/foundations/starting-workflows.mdx +211 -0
  130. package/docs/foundations/streaming.mdx +569 -0
  131. package/docs/foundations/workflows-and-steps.mdx +197 -0
  132. package/docs/how-it-works/code-transform.mdx +334 -0
  133. package/docs/how-it-works/event-sourcing.mdx +254 -0
  134. package/docs/how-it-works/framework-integrations.mdx +437 -0
  135. package/docs/how-it-works/meta.json +10 -0
  136. package/docs/how-it-works/understanding-directives.mdx +611 -0
  137. package/package.json +31 -25
  138. package/dist/builtins.js.map +0 -1
  139. package/dist/create-hook.js.map +0 -1
  140. package/dist/define-hook.js.map +0 -1
  141. package/dist/events-consumer.js.map +0 -1
  142. package/dist/global.js.map +0 -1
  143. package/dist/index.js.map +0 -1
  144. package/dist/logger.js.map +0 -1
  145. package/dist/observability.js.map +0 -1
  146. package/dist/parse-name.d.ts +0 -25
  147. package/dist/parse-name.d.ts.map +0 -1
  148. package/dist/parse-name.js +0 -40
  149. package/dist/parse-name.js.map +0 -1
  150. package/dist/private.js.map +0 -1
  151. package/dist/runtime/resume-hook.js.map +0 -1
  152. package/dist/runtime/start.js.map +0 -1
  153. package/dist/runtime/world.js.map +0 -1
  154. package/dist/runtime.js.map +0 -1
  155. package/dist/schemas.js.map +0 -1
  156. package/dist/serialization.js.map +0 -1
  157. package/dist/sleep.js.map +0 -1
  158. package/dist/step/context-storage.js.map +0 -1
  159. package/dist/step/get-step-metadata.js.map +0 -1
  160. package/dist/step/get-workflow-metadata.js.map +0 -1
  161. package/dist/step.js.map +0 -1
  162. package/dist/symbols.js.map +0 -1
  163. package/dist/telemetry/semantic-conventions.js.map +0 -1
  164. package/dist/telemetry.js.map +0 -1
  165. package/dist/types.js.map +0 -1
  166. package/dist/util.js.map +0 -1
  167. package/dist/vm/index.js.map +0 -1
  168. package/dist/vm/uuid.js.map +0 -1
  169. package/dist/workflow/create-hook.js.map +0 -1
  170. package/dist/workflow/define-hook.js.map +0 -1
  171. package/dist/workflow/get-workflow-metadata.js.map +0 -1
  172. package/dist/workflow/hook.js.map +0 -1
  173. package/dist/workflow/index.js.map +0 -1
  174. package/dist/workflow/sleep.js.map +0 -1
  175. package/dist/workflow/writable-stream.js.map +0 -1
  176. package/dist/workflow.js.map +0 -1
  177. package/dist/writable-stream.d.ts.map +0 -1
  178. package/dist/writable-stream.js +0 -16
  179. package/dist/writable-stream.js.map +0 -1
@@ -0,0 +1,455 @@
1
+ ---
2
+ title: Hooks & Webhooks
3
+ ---
4
+
5
+ Hooks provide a powerful mechanism for pausing workflow execution and resuming it later with external data. They enable workflows to wait for external events, user interactions (also known as "human in the loop"), or HTTP requests. This guide will teach you the core concepts, starting with the low-level Hook primitive and building up to the higher-level Webhook abstraction.
6
+
7
+ ## Understanding Hooks
8
+
9
+ At their core, **Hooks** are a low-level primitive that allows you to pause a workflow and resume it later with arbitrary [serializable data](/docs/foundations/serialization). Think of them as suspension points in your workflow where you're waiting for external input.
10
+
11
+ When you create a hook, it generates a unique token that external systems can use to send data back to your workflow. This makes hooks perfect for scenarios like:
12
+
13
+ - Waiting for approval from a user or admin
14
+ - Receiving data from an external system or service
15
+ - Implementing event-driven workflows that react to multiple events over time
16
+
17
+ ### Creating Your First Hook
18
+
19
+ Let's start with a simple example. Here's a workflow that creates a hook and waits for external data:
20
+
21
+ ```typescript lineNumbers
22
+ import { createHook } from "workflow";
23
+
24
+ export async function approvalWorkflow() {
25
+ "use workflow";
26
+
27
+ // Create a hook that expects an approval payload
28
+ const hook = createHook<{ approved: boolean; comment: string }>();
29
+
30
+ console.log("Waiting for approval...");
31
+ console.log("Send approval to token:", hook.token);
32
+
33
+ // Workflow pauses here until data is sent
34
+ const result = await hook;
35
+
36
+ if (result.approved) {
37
+ console.log("Approved with comment:", result.comment);
38
+ // Continue with approved workflow...
39
+ } else {
40
+ console.log("Rejected:", result.comment);
41
+ // Handle rejection...
42
+ }
43
+ }
44
+ ```
45
+
46
+ The workflow will pause at `await hook` until external code sends data to resume it.
47
+
48
+ <Callout type="info">
49
+ See the full API reference for [`createHook()`](/docs/api-reference/workflow/create-hook) for all available options.
50
+ </Callout>
51
+
52
+ ### Resuming a Hook
53
+
54
+ To send data to a waiting workflow, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) from an API route, server action, or any other external context:
55
+
56
+ ```typescript lineNumbers
57
+ import { resumeHook } from "workflow/api";
58
+
59
+ // In an API route or external handler
60
+ export async function POST(request: Request) {
61
+ const { token, approved, comment } = await request.json();
62
+
63
+ try {
64
+ // Resume the workflow with the approval data
65
+ const result = await resumeHook(token, { approved, comment });
66
+ return Response.json({ success: true, runId: result.runId });
67
+ } catch (error) {
68
+ return Response.json({ error: "Invalid token" }, { status: 404 });
69
+ }
70
+ }
71
+ ```
72
+
73
+ The key points:
74
+ - Hooks allow you to pass **any [serializable data](/docs/foundations/serialization)** as the payload
75
+ - You need the hook's `token` to resume it
76
+ - The workflow will resume execution right where it left off
77
+
78
+ ### Custom Tokens for Deterministic Hooks
79
+
80
+ By default, hooks generate a random token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
81
+
82
+ For example, imagine a Slack bot where each channel should have its own workflow instance:
83
+
84
+ ```typescript lineNumbers
85
+ import { createHook } from "workflow";
86
+
87
+ export async function slackChannelBot(channelId: string) {
88
+ "use workflow";
89
+
90
+ // Use channel ID in the token so Slack webhooks can find this workflow
91
+ const hook = createHook<SlackMessage>({
92
+ token: `slack_messages:${channelId}`
93
+ });
94
+
95
+ for await (const message of hook) {
96
+ console.log(`${message.user}: ${message.text}`);
97
+
98
+ if (message.text === "/stop") {
99
+ break;
100
+ }
101
+
102
+ await processMessage(message);
103
+ }
104
+ }
105
+
106
+ async function processMessage(message: SlackMessage) {
107
+ "use step";
108
+ // Process the Slack message
109
+ }
110
+ ```
111
+
112
+ Now your Slack webhook handler can deterministically resume the correct workflow:
113
+
114
+ ```typescript lineNumbers
115
+ import { resumeHook } from "workflow/api";
116
+
117
+ export async function POST(request: Request) {
118
+ const slackEvent = await request.json();
119
+ const channelId = slackEvent.channel;
120
+
121
+ try {
122
+ // Reconstruct the token using the channel ID
123
+ await resumeHook(`slack_messages:${channelId}`, slackEvent);
124
+
125
+ return new Response("OK");
126
+ } catch (error) {
127
+ return new Response("Hook not found", { status: 404 });
128
+ }
129
+ }
130
+ ```
131
+
132
+ ### Receiving Multiple Events
133
+
134
+ Hooks are _reusable_ - they implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
135
+
136
+ ```typescript lineNumbers
137
+ import { createHook } from "workflow";
138
+
139
+ export async function dataCollectionWorkflow() {
140
+ "use workflow";
141
+
142
+ const hook = createHook<{ value: number; done?: boolean }>();
143
+
144
+ const values: number[] = [];
145
+
146
+ // Keep receiving data until we get a "done" signal
147
+ for await (const payload of hook) {
148
+ values.push(payload.value);
149
+
150
+ if (payload.done) {
151
+ break;
152
+ }
153
+ }
154
+
155
+ console.log("Collected values:", values);
156
+ return values;
157
+ }
158
+ ```
159
+
160
+ Each time you call `resumeHook()` with the same token, the loop receives another value.
161
+
162
+ ## Understanding Webhooks
163
+
164
+ While hooks are powerful, they require you to manually handle HTTP requests and route them to workflows. **Webhooks** solve this by providing a higher-level abstraction built on top of hooks that:
165
+
166
+ 1. Automatically serializes the entire HTTP [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
167
+ 2. Provides an automatically addressable `url` property pointing to the generated webhook endpoint
168
+ 3. Handles sending HTTP [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects back to the caller
169
+
170
+ When using Workflow DevKit, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
171
+
172
+ <Callout type="info">
173
+ See the full API reference for [`createWebhook()`](/docs/api-reference/workflow/create-webhook) for all available options.
174
+ </Callout>
175
+
176
+ ### Creating Your First Webhook
177
+
178
+ Here's a simple webhook that receives HTTP requests:
179
+
180
+ ```typescript lineNumbers
181
+ import { createWebhook } from "workflow";
182
+
183
+ export async function webhookWorkflow() {
184
+ "use workflow";
185
+
186
+ const webhook = createWebhook();
187
+
188
+ // The webhook is automatically available at this URL
189
+ console.log("Send HTTP requests to:", webhook.url);
190
+ // Example: https://your-app.com/.well-known/workflow/v1/webhook/lJHkuMdQ2FxSFTbUMU84k
191
+
192
+ // Workflow pauses until an HTTP request is received
193
+ const request = await webhook;
194
+
195
+ console.log("Received request:", request.method, request.url);
196
+
197
+ // Access the request body
198
+ const data = await request.json();
199
+ console.log("Data:", data);
200
+ }
201
+ ```
202
+
203
+ The webhook will automatically respond with a `202 Accepted` status by default. External systems can simply make an HTTP request to the `webhook.url` to resume your workflow.
204
+
205
+ ### Sending Custom Responses
206
+
207
+ Webhooks provide two ways to send custom HTTP responses: **static responses** and **dynamic responses**.
208
+
209
+ #### Static Responses
210
+
211
+ Use the `respondWith` option to provide a static response that will be sent automatically for every request:
212
+
213
+ ```typescript lineNumbers
214
+ import { createWebhook } from "workflow";
215
+
216
+ export async function webhookWithStaticResponse() {
217
+ "use workflow";
218
+
219
+ const webhook = createWebhook({
220
+ respondWith: Response.json({
221
+ success: true, message: "Webhook received"
222
+ }),
223
+ });
224
+
225
+ const request = await webhook;
226
+
227
+ // The response was already sent automatically
228
+ // Continue processing the request asynchronously
229
+ const data = await request.json();
230
+ await processData(data);
231
+ }
232
+
233
+ async function processData(data: any) {
234
+ "use step";
235
+ // Long-running processing here
236
+ }
237
+ ```
238
+
239
+ #### Dynamic Responses (Manual Mode)
240
+
241
+ For dynamic responses based on the request content, set `respondWith: "manual"` and call the `respondWith()` method on the request:
242
+
243
+ ```typescript lineNumbers
244
+ import { createWebhook, type RequestWithResponse } from "workflow";
245
+
246
+ async function sendCustomResponse(request: RequestWithResponse, message: string) {
247
+ "use step";
248
+
249
+ // Call respondWith() to send the response
250
+ await request.respondWith(
251
+ new Response(
252
+ JSON.stringify({ message }),
253
+ {
254
+ status: 200,
255
+ headers: { "Content-Type": "application/json" }
256
+ }
257
+ )
258
+ );
259
+ }
260
+
261
+ export async function webhookWithDynamicResponse() {
262
+ "use workflow";
263
+
264
+ // Set respondWith to "manual" to handle responses yourself
265
+ const webhook = createWebhook({ respondWith: "manual" });
266
+
267
+ const request = await webhook;
268
+ const data = await request.json();
269
+
270
+ // Decide what response to send based on the data
271
+ if (data.type === "urgent") {
272
+ await sendCustomResponse(request, "Processing urgently");
273
+ } else {
274
+ await sendCustomResponse(request, "Processing normally");
275
+ }
276
+
277
+ // Continue workflow...
278
+ }
279
+ ```
280
+
281
+ <Callout type="warning">
282
+ When using `respondWith: "manual"`, the `respondWith()` method **must** be called from within a step function due to serialization requirements. This requirement may be removed in the future.
283
+ </Callout>
284
+
285
+ ### Handling Multiple Webhook Requests
286
+
287
+ Like hooks, webhooks support iteration:
288
+
289
+ ```typescript lineNumbers
290
+ import { createWebhook, type RequestWithResponse } from "workflow";
291
+
292
+ async function respondToSlack(request: RequestWithResponse, text: string) {
293
+ "use step";
294
+
295
+ await request.respondWith(
296
+ new Response(
297
+ JSON.stringify({ response_type: "in_channel", text }),
298
+ { headers: { "Content-Type": "application/json" } }
299
+ )
300
+ );
301
+ }
302
+
303
+ export async function slackCommandWorkflow(channelId: string) {
304
+ "use workflow";
305
+
306
+ const webhook = createWebhook({
307
+ token: `slack_command:${channelId}`,
308
+ respondWith: "manual"
309
+ });
310
+
311
+ console.log("Configure Slack command webhook:", webhook.url);
312
+
313
+ for await (const request of webhook) {
314
+ const formData = await request.formData();
315
+ const command = formData.get("command");
316
+ const text = formData.get("text");
317
+
318
+ if (command === "/status") {
319
+ await respondToSlack(request, "Checking status...");
320
+ const status = await checkSystemStatus();
321
+ await postToSlack(channelId, `Status: ${status}`);
322
+ }
323
+
324
+ if (text === "stop") {
325
+ await respondToSlack(request, "Stopping workflow...");
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ async function checkSystemStatus() {
332
+ "use step";
333
+ return "All systems operational";
334
+ }
335
+
336
+ async function postToSlack(channelId: string, message: string) {
337
+ "use step";
338
+ // Post message to Slack
339
+ }
340
+ ```
341
+
342
+ ## Hooks vs. Webhooks: When to Use Each
343
+
344
+ | Feature | Hooks | Webhooks |
345
+ |---------|-------|----------|
346
+ | **Data Format** | Arbitrary serializable data | HTTP `Request` objects |
347
+ | **URL** | No automatic URL | Automatic `webhook.url` property |
348
+ | **Response Handling** | N/A | Can send HTTP `Response` (static or dynamic) |
349
+ | **Use Case** | Custom integrations, type-safe payloads | HTTP webhooks, standard REST APIs |
350
+ | **Resuming** | [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) | Automatic via HTTP, or [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) |
351
+
352
+ **Use Hooks when:**
353
+ - You need full control over the payload structure
354
+ - You're integrating with custom event sources
355
+ - You want strong TypeScript typing with [`defineHook()`](/docs/api-reference/workflow/define-hook)
356
+
357
+ **Use Webhooks when:**
358
+ - You're receiving HTTP requests from external services
359
+ - You need to send HTTP responses back to the caller
360
+ - You want automatic URL routing without writing API handlers
361
+
362
+ ## Advanced Patterns
363
+
364
+ ### Type-Safe Hooks with `defineHook()`
365
+
366
+ The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot:
367
+
368
+ ```typescript lineNumbers
369
+ import { defineHook } from "workflow";
370
+ import { z } from "zod";
371
+
372
+ // Define the hook with schema for type safety and runtime validation
373
+ const approvalHook = defineHook({ // [!code highlight]
374
+ schema: z.object({ // [!code highlight]
375
+ requestId: z.string(), // [!code highlight]
376
+ approved: z.boolean(), // [!code highlight]
377
+ approvedBy: z.string(), // [!code highlight]
378
+ comment: z.string().transform((value) => value.trim()), // [!code highlight]
379
+ }), // [!code highlight]
380
+ }); // [!code highlight]
381
+
382
+ // In your workflow
383
+ export async function documentApprovalWorkflow(documentId: string) {
384
+ "use workflow";
385
+
386
+ const hook = approvalHook.create({
387
+ token: `approval:${documentId}`
388
+ });
389
+
390
+ // Payload is type-safe and validated
391
+ const approval = await hook;
392
+
393
+ console.log(`Document ${approval.requestId} ${approval.approved ? "approved" : "rejected"}`);
394
+ console.log(`By: ${approval.approvedBy}, Comment: ${approval.comment}`);
395
+ }
396
+
397
+ // In your API route - both type-safe and runtime-validated!
398
+ export async function POST(request: Request) {
399
+ const { documentId, ...approvalData } = await request.json();
400
+
401
+ try {
402
+ // The schema validates the payload before resuming the workflow
403
+ await approvalHook.resume(`approval:${documentId}`, approvalData);
404
+ return new Response("OK");
405
+ } catch (error) {
406
+ return Response.json({ error: "Invalid token or validation failed" }, { status: 400 });
407
+ }
408
+ }
409
+ ```
410
+
411
+ This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.
412
+
413
+ ## Best Practices
414
+
415
+ ### Token Design
416
+
417
+ When using custom tokens:
418
+
419
+ - **Make them deterministic**: Base them on data the external system can reconstruct (like channel IDs, user IDs, etc.)
420
+ - **Use namespacing**: Prefix tokens to avoid conflicts (e.g., `slack:${channelId}`, `github:${repoId}`)
421
+ - **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance
422
+
423
+ ### Response Handling in Webhooks
424
+
425
+ - Use **static responses** (`respondWith: Response`) for simple acknowledgments
426
+ - Use **manual mode** (`respondWith: "manual"`) when responses depend on request processing
427
+ - Remember that `respondWith()` must be called from within a step function
428
+
429
+ ### Iterating Over Events
430
+
431
+ Both hooks and webhooks support iteration, making them perfect for long-running event loops:
432
+
433
+ {/* @skip-typecheck: incomplete code sample */}
434
+ ```typescript
435
+ const hook = createHook<Event>();
436
+
437
+ for await (const event of hook) {
438
+ await processEvent(event);
439
+
440
+ if (shouldStop(event)) {
441
+ break;
442
+ }
443
+ }
444
+ ```
445
+
446
+ This pattern allows a single workflow instance to handle multiple events over time, maintaining state between events.
447
+
448
+ ## Related Documentation
449
+
450
+ - [Serialization](/docs/foundations/serialization) - Understanding what data can be passed through hooks
451
+ - [`createHook()` API Reference](/docs/api-reference/workflow/create-hook)
452
+ - [`createWebhook()` API Reference](/docs/api-reference/workflow/create-webhook)
453
+ - [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook)
454
+ - [`resumeHook()` API Reference](/docs/api-reference/workflow-api/resume-hook)
455
+ - [`resumeWebhook()` API Reference](/docs/api-reference/workflow-api/resume-webhook)
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: Idempotency
3
+ ---
4
+
5
+ Idempotency is a property of an operation that ensures it can be safely retried without producing duplicate side effects.
6
+
7
+ In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once just by seeing if it succeeds.
8
+ Consider a payment API that charges the user $10, but due to network failures, the confirmation response is lost. When the step retries (because the previous attempt was considered a failure), it will charge the user again.
9
+
10
+ To prevent this, many external APIs support idempotency keys. An idempotency key is a unique identifier for an operation that can be used to deduplicate requests.
11
+
12
+ ## The core pattern: use the step ID as your idempotency key
13
+
14
+ Every step invocation has a stable `stepId` that stays the same across retries.
15
+ Use it as the idempotency key when calling third-party APIs.
16
+
17
+ ```typescript lineNumbers
18
+ import { getStepMetadata } from "workflow";
19
+
20
+ async function chargeUser(userId: string, amount: number) {
21
+ "use step";
22
+
23
+ const { stepId } = getStepMetadata();
24
+
25
+ // Example: Stripe-style idempotency key
26
+ // This guarantees only one charge is created even if the step retries
27
+ await stripe.charges.create(
28
+ {
29
+ amount,
30
+ currency: "usd",
31
+ customer: userId,
32
+ },
33
+ {
34
+ idempotencyKey: stepId, // [!code highlight]
35
+ }
36
+ );
37
+ }
38
+ ```
39
+
40
+ Why this works:
41
+
42
+ - **Stable across retries**: `stepId` does not change between attempts.
43
+ - **Globally unique per step**: Fulfills the uniqueness requirement for an idempotency key.
44
+
45
+ ## Best practices
46
+
47
+ - **Always provide idempotency keys to external side effects that are not idempotent** inside steps (payments, emails, SMS, queues).
48
+ - **Prefer `stepId` as your key**; it is stable across retries and unique per step.
49
+ - **Keep keys deterministic**; avoid including timestamps or attempt counters.
50
+ - **Handle 409/conflict responses** gracefully; treat them as success if the prior attempt completed.
51
+
52
+ ## Related docs
53
+
54
+ - Learn about retries in [Errors & Retrying](/docs/foundations/errors-and-retries)
55
+ - API reference: [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata)
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: Foundations
3
+ ---
4
+
5
+ Workflow programming can be a slight shift from how you traditionally write real-world applications. Learning the foundations now will go a long way toward helping you use workflows effectively.
6
+
7
+ <Cards>
8
+ <Card href="/docs/foundations/workflows-and-steps" title="Workflows and Steps">
9
+ Learn about the building blocks of durability
10
+ </Card>
11
+ <Card href="/docs/foundations/starting-workflows" title="Starting Workflows">
12
+ Trigger workflows and track their execution using the `start()` function.
13
+ </Card>
14
+ <Card href="/docs/foundations/common-patterns" title="Common Patterns">
15
+ Common patterns useful in workflows.
16
+ </Card>
17
+ <Card href="/docs/foundations/errors-and-retries" title="Errors & Retrying">
18
+ Types of errors and how retrying work in workflows.
19
+ </Card>
20
+ <Card href="/docs/foundations/hooks" title="Webhooks (and hooks)">
21
+ Respond to external events in your workflow using hooks and webhooks.
22
+ </Card>
23
+ <Card href="/docs/foundations/streaming" title="Streaming">
24
+ Stream data in real-time to clients without waiting for the workflow to complete.
25
+ </Card>
26
+ <Card href="/docs/foundations/serialization" title="Serialization">
27
+ Understand which types can be passed between workflow and step functions.
28
+ </Card>
29
+ <Card href="/docs/foundations/idempotency" title="Idempotency">
30
+ Prevent duplicate side effects when retrying operations.
31
+ </Card>
32
+ </Cards>
@@ -0,0 +1,14 @@
1
+ {
2
+ "title": "Foundations",
3
+ "pages": [
4
+ "workflows-and-steps",
5
+ "starting-workflows",
6
+ "common-patterns",
7
+ "errors-and-retries",
8
+ "hooks",
9
+ "streaming",
10
+ "serialization",
11
+ "idempotency"
12
+ ],
13
+ "defaultOpen": true
14
+ }