workflow 5.0.0 → 5.1.0

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 (59) hide show
  1. package/docs/advanced/dynamic-workflows.mdx +4 -1
  2. package/docs/advanced/index.mdx +13 -0
  3. package/docs/advanced/meta.json +5 -0
  4. package/docs/ai/chat-session-modeling.mdx +8 -8
  5. package/docs/ai/human-in-the-loop.mdx +7 -3
  6. package/docs/ai/index.mdx +14 -14
  7. package/docs/ai/streaming-updates-from-tools.mdx +2 -2
  8. package/docs/api-reference/vitest/index.mdx +2 -2
  9. package/docs/api-reference/workflow/create-hook.mdx +4 -0
  10. package/docs/api-reference/workflow/create-webhook.mdx +1 -1
  11. package/docs/api-reference/workflow/define-hook.mdx +4 -0
  12. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +9 -3
  13. package/docs/api-reference/workflow-api/resume-hook.mdx +4 -0
  14. package/docs/api-reference/workflow-api/resume-webhook.mdx +4 -0
  15. package/docs/api-reference/workflow-globals.mdx +4 -0
  16. package/docs/api-reference/workflow-nest/index.mdx +4 -1
  17. package/docs/api-reference/workflow-nest/is-workflow-request.mdx +58 -0
  18. package/docs/api-reference/workflow-nest/meta.json +1 -0
  19. package/docs/api-reference/workflow-nest/workflow-controller.mdx +6 -2
  20. package/docs/api-reference/workflow-nest/workflow-module.mdx +9 -1
  21. package/docs/api-reference/workflow-runtime/world/storage.mdx +29 -0
  22. package/docs/configuration/build-and-diagnostics.mdx +11 -1
  23. package/docs/configuration/framework-options.mdx +6 -0
  24. package/docs/configuration/runtime-tuning.mdx +28 -2
  25. package/docs/configuration/worlds.mdx +28 -2
  26. package/docs/cookbook/common-patterns/webhooks.mdx +2 -1
  27. package/docs/cookbook/integrations/ai-sdk.mdx +8 -8
  28. package/docs/cookbook/integrations/chat-sdk.mdx +8 -8
  29. package/docs/cookbook/integrations/sandbox.mdx +8 -8
  30. package/docs/errors/node-js-module-in-workflow.mdx +36 -0
  31. package/docs/foundations/errors-and-retries.mdx +3 -3
  32. package/docs/foundations/hooks.mdx +91 -3
  33. package/docs/foundations/serialization.mdx +1 -1
  34. package/docs/foundations/streaming.mdx +1 -1
  35. package/docs/foundations/workflows-and-steps.mdx +1 -1
  36. package/docs/getting-started/astro.mdx +7 -9
  37. package/docs/getting-started/express.mdx +4 -10
  38. package/docs/getting-started/fastify.mdx +4 -9
  39. package/docs/getting-started/hono.mdx +4 -10
  40. package/docs/getting-started/index.mdx +2 -2
  41. package/docs/getting-started/nestjs.mdx +157 -36
  42. package/docs/getting-started/next.mdx +15 -18
  43. package/docs/getting-started/nitro.mdx +4 -10
  44. package/docs/getting-started/nuxt.mdx +4 -10
  45. package/docs/getting-started/react-router/v7.mdx +6 -22
  46. package/docs/getting-started/react-router/v8.mdx +6 -22
  47. package/docs/getting-started/sveltekit.mdx +7 -9
  48. package/docs/getting-started/tanstack-start.mdx +7 -9
  49. package/docs/getting-started/vite.mdx +7 -9
  50. package/docs/how-it-works/code-transform.mdx +13 -9
  51. package/docs/how-it-works/encryption.mdx +3 -1
  52. package/docs/meta.json +1 -1
  53. package/docs/testing/index.mdx +3 -5
  54. package/docs/whats-new.mdx +8 -2
  55. package/docs/worlds/building-a-world.mdx +68 -1
  56. package/docs/worlds/postgres.mdx +16 -32
  57. package/docs/worlds/upgrading-to-v5.mdx +29 -8
  58. package/docs/worlds/vercel.mdx +50 -4
  59. package/package.json +12 -12
@@ -160,6 +160,12 @@ The Postgres World is a self-hosted durable backend for long-running server proc
160
160
  - Number of concurrent workers polling for jobs.
161
161
  - Also bounds concurrent parent-to-child workflow return-value polls.
162
162
 
163
+ ### `pollInterval`
164
+
165
+ - Environment variable: `WORKFLOW_POSTGRES_POLL_INTERVAL_MS`
166
+ - Default: `500`
167
+ - Milliseconds between idle job fetches per worker.
168
+
163
169
  ### `applicationManagedShutdown`
164
170
 
165
171
  - Environment variable: `WORKFLOW_POSTGRES_APPLICATION_MANAGED_SHUTDOWN` (`1` enables)
@@ -290,6 +296,7 @@ Platform-provided values such as `VERCEL_DEPLOYMENT_ID`, `VERCEL_PROJECT_ID`, an
290
296
  - Default: `http`
291
297
  - Experimental stream-write transport capability. Set to exactly `ws` to attempt `workflow-stream-ws/v1`. The server authoritatively accepts or declines each upgrade; a decline uses HTTP directly for that writer lifetime. Stream reads remain HTTP and demand-driven.
292
298
  - This is not tenant rollout policy or a package-version check. HTTP remains the compatibility path. `/websockets/v1` is independent of REST v2/v4 and persisted workflow `specVersion` values.
299
+ - A throttled (429) socket write or close is retried after the server's `Retry-After`, as over HTTP. It moves to HTTP if the connection ends during the wait or the cumulative wait passes 30 seconds.
293
300
 
294
301
  ### `WORKFLOW_DISABLE_ANALYTICS_READS`
295
302
 
@@ -310,6 +317,25 @@ When enabled (the default), a suspension's eager `step_created` and `wait_create
310
317
 
311
318
  - Factory option: none
312
319
  - CLI flag: none
313
- - Default: `http`
314
- - Set to `ws` to ship workflow run events to the Vercel World over a WebSocket instead of one HTTP request each. Only `ws` (case-insensitive) opts in; any other value, including unset, empty, or `http`, keeps HTTP.
320
+ - Default: `ws`
321
+ - Ships workflow run events to the Vercel World over a WebSocket instead of one HTTP request each. Set to exactly `http` to opt out; any other value, including unset or empty, uses the WebSocket.
315
322
  - Ignored when the World is configured with `projectConfig` and routes through the `api-workflow` proxy: that endpoint is an HTTP-only REST gateway and does not forward a WebSocket upgrade, so events stay on HTTP.
323
+
324
+ ### `WORKFLOW_EVENTS_TRANSPORT_WS_OVERRIDE_WORKFLOWS`
325
+
326
+ - Factory option: none
327
+ - CLI flag: none
328
+ - Default: none
329
+ - Comma-separated workflows whose runs use the WebSocket events transport even when `WORKFLOW_EVENTS_TRANSPORT` is `http`. Each entry is a function name (`processOrder`) or a full workflow name (`workflow//./src/workflows/order//processOrder`), matched exactly and case-sensitively.
330
+ - Has no effect unless `WORKFLOW_EVENTS_TRANSPORT=http`, since every workflow already uses the WebSocket by default.
331
+ - See [`WORKFLOW_EVENTS_TRANSPORT_WS_OVERRIDE_WORKFLOWS`](/worlds/vercel#workflow_events_transport_ws_override_workflows) for details.
332
+
333
+ ### `WORKFLOW_WS_MAX_MESSAGE_BYTES`
334
+
335
+ - Factory option: none
336
+ - CLI flag: none
337
+ - Default: `12582912` (12 MiB)
338
+ - Clamp: `2097152` to `16777216` (values outside are clamped, with a warning)
339
+ - Largest WebSocket message the Vercel World sends, header included, for both the events and stream-write transports. The ceiling is the 16 MiB WebSocket message limit.
340
+ - Events: a larger frame is sent as several messages and rebuilt on the other side.
341
+ - Stream writes: a larger write group is split into several ordered write requests; a single chunk too large for one message is written over HTTP, as are the rest of that writer's writes.
@@ -50,7 +50,7 @@ export async function paymentWebhook(orderId: string) {
50
50
 
51
51
  ### Step function for processing
52
52
 
53
- Each webhook request is processed in its own step, giving you full Node.js access for validation, database writes, and responding to the caller:
53
+ Each webhook request is processed in its own step, giving you full Node.js access for validation, database writes, and responding to the caller. In production, verify the provider's signature here before trusting the body; the example omits it for brevity.
54
54
 
55
55
  ```typescript
56
56
  import { type RequestWithResponse } from "workflow";
@@ -176,6 +176,7 @@ export async function POST(request: Request) {
176
176
  - **`respondWith: "manual"`** gives you control over the HTTP response from inside a step. Use this when you need to validate the request before responding.
177
177
  - **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
178
178
  - **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
179
+ - **The URL is the only authorization.** Anyone with the webhook URL can send it a request, so verify the provider's signature in the processing step before acting on the payload. See [Hook and webhook security](/docs/foundations/hooks#security).
179
180
  - **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
180
181
  - **For large payloads**, use a hook and reference token instead of passing the data through the workflow. The event log serializes all step inputs and outputs, so large payloads hurt performance.
181
182
 
@@ -41,9 +41,9 @@ One workflow run represents one full conversation. The workflow suspends between
41
41
  Because the conversation is one workflow run, it stays on the deployment that started it. If each turn should run on the latest deployment while preserving selected state or streams, see [Versioning](/docs/foundations/versioning) for the child-run continuation pattern.
42
42
  </Callout>
43
43
 
44
- <Tabs items={['Workflow', 'API Route', 'Client']}>
44
+ <TabsWithChildren tabs={["Workflow","API Route","Client"]}>
45
45
 
46
- <Tab value="Workflow">
46
+ <TabContent order={1}>
47
47
 
48
48
  ```typescript title="workflows/support.ts" lineNumbers
49
49
  import { streamText, stepCountIs } from "ai";
@@ -129,9 +129,9 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
129
129
  }
130
130
  ```
131
131
 
132
- </Tab>
132
+ </TabContent>
133
133
 
134
- <Tab value="API Route">
134
+ <TabContent order={2}>
135
135
 
136
136
  One endpoint handles first turn, follow-ups, and the `/done` exit. The client sends `runId` in the body to distinguish first vs follow-up.
137
137
 
@@ -246,9 +246,9 @@ export async function POST(req: Request) {
246
246
  }
247
247
  ```
248
248
 
249
- </Tab>
249
+ </TabContent>
250
250
 
251
- <Tab value="Client">
251
+ <TabContent order={3}>
252
252
 
253
253
  Store the `runId` in a ref and pass it in the body of every follow-up. `WorkflowChatTransport` forwards it for you.
254
254
 
@@ -299,9 +299,9 @@ export function SupportChat() {
299
299
  }
300
300
  ```
301
301
 
302
- </Tab>
302
+ </TabContent>
303
303
 
304
- </Tabs>
304
+ </TabsWithChildren>
305
305
 
306
306
  ## How it works
307
307
 
@@ -66,9 +66,9 @@ Because the session *is* a workflow run, its history is recoverable from the eve
66
66
 
67
67
  This pattern uses three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
68
68
 
69
- <Tabs items={['Bot Setup', 'Workflow', 'Event Handlers']}>
69
+ <TabsWithChildren tabs={["Bot Setup","Workflow","Event Handlers"]}>
70
70
 
71
- <Tab value="Bot Setup">
71
+ <TabContent order={1}>
72
72
 
73
73
  Register the `Chat` instance as a singleton so step functions can dynamically import it and resolve adapters + state:
74
74
 
@@ -95,9 +95,9 @@ export const bot = new Chat<typeof adapters, ThreadState>({
95
95
 
96
96
  `registerSingleton()` is important: Chat SDK re-hydrates `Thread` objects inside step functions, and it needs a registered singleton to resolve adapters and state for those rehydrated instances.
97
97
 
98
- </Tab>
98
+ </TabContent>
99
99
 
100
- <Tab value="Workflow">
100
+ <TabContent order={2}>
101
101
 
102
102
  The workflow is a plain loop over a hook. It receives the serialized thread + first message from the handler, revives them via Chat SDK's standalone `reviver`, and every platform-side effect goes inside a `"use step"` helper:
103
103
 
@@ -174,9 +174,9 @@ export type ChatTurnPayload = {
174
174
  };
175
175
  ```
176
176
 
177
- </Tab>
177
+ </TabContent>
178
178
 
179
- <Tab value="Event Handlers">
179
+ <TabContent order={3}>
180
180
 
181
181
  Handlers live outside the workflow file so adapter dependencies don't leak in. They decide whether to start a new workflow or resume an existing one, then store the `runId` in thread state:
182
182
 
@@ -256,9 +256,9 @@ export async function POST(
256
256
  }
257
257
  ```
258
258
 
259
- </Tab>
259
+ </TabContent>
260
260
 
261
- </Tabs>
261
+ </TabsWithChildren>
262
262
 
263
263
  ## How it works
264
264
 
@@ -88,9 +88,9 @@ When the timer wins:
88
88
 
89
89
  The only way out is an explicit `/destroy` command.
90
90
 
91
- <Tabs items={['Workflow', 'API Routes', 'Client']}>
91
+ <TabsWithChildren tabs={["Workflow","API Routes","Client"]}>
92
92
 
93
- <Tab value="Workflow">
93
+ <TabContent order={1}>
94
94
 
95
95
  ```typescript title="workflows/sandbox-session.ts" lineNumbers
96
96
  import { defineHook, sleep, getWritable, getWorkflowMetadata } from "workflow";
@@ -298,9 +298,9 @@ export async function sandboxSessionWorkflow() {
298
298
  }
299
299
  ```
300
300
 
301
- </Tab>
301
+ </TabContent>
302
302
 
303
- <Tab value="API Routes">
303
+ <TabContent order={2}>
304
304
 
305
305
  Two endpoints manage the session. `/start` accepts an optional `{ runId }`: if the run still exists, it replays the event log from index 0 so a returning client fully rehydrates. `/command` resumes the hook and returns immediately; command output lands on the `/start` stream.
306
306
 
@@ -379,9 +379,9 @@ export async function POST(req: Request) {
379
379
  }
380
380
  ```
381
381
 
382
- </Tab>
382
+ </TabContent>
383
383
 
384
- <Tab value="Client">
384
+ <TabContent order={3}>
385
385
 
386
386
  On mount, reconnect to the existing run if `localStorage` contains a `runId`. Otherwise, start a new run. Send commands to `/command` with POST requests. Output arrives on the `/start` stream.
387
387
 
@@ -472,9 +472,9 @@ export function SandboxRunner() {
472
472
  }
473
473
  ```
474
474
 
475
- </Tab>
475
+ </TabContent>
476
476
 
477
- </Tabs>
477
+ </TabsWithChildren>
478
478
 
479
479
  ## How it works
480
480
 
@@ -27,6 +27,42 @@ Workflow functions run in a sandboxed environment without full Node.js runtime a
27
27
 
28
28
  Node.js modules have side effects and non-deterministic behavior that could break workflow replay guarantees.
29
29
 
30
+ ## Transitive dependencies and dynamic `require()`
31
+
32
+ The same error is reported for a module the workflow bundle could not inline, even when your own code never imports it directly:
33
+
34
+ ```text
35
+ Workflow bundle cannot run in the workflow sandbox.
36
+
37
+ Imports left external (1):
38
+ • "node:fs"
39
+ imported by node_modules/leaky-pkg/index.js
40
+ via app/workflows/order.ts → node_modules/wrapper-pkg/index.js → node_modules/leaky-pkg/index.js
41
+
42
+ Unresolved require() calls (1):
43
+ • dynamic require() in node_modules/lazy-pkg/index.js (bundle 412:31)
44
+ ```
45
+
46
+ The workflow sandbox has no `require`, so both would throw `ReferenceError: require is not defined` when the bundle loads. Follow the import chain to the first file you own and move that work into a step function, or replace a dynamic `require()` with a static import so the bundler can inline it.
47
+
48
+ A `require()` wrapped in `try`/`catch`, or behind a `typeof require` check, is not reported — that is how packages probe for an optional dependency or for a CommonJS environment, and in the sandbox the error is caught or the call never runs:
49
+
50
+ ```js
51
+ try {
52
+ loadOptional(require("@emotion/is-prop-valid").default);
53
+ } catch {
54
+ // optional dependency, fall back
55
+ }
56
+
57
+ if (typeof require !== "undefined") {
58
+ crypto = require("crypto");
59
+ }
60
+ ```
61
+
62
+ A `typeof require` check only excuses the branch it skips when `require` is undefined, so `typeof require === "undefined" ? require(x) : y` is still reported. A `try` only counts when it has a `catch`. The checks read the code's structure without running it, so a `catch` that rethrows the error is still treated as a guard, and the bundle fails at runtime if that code is reached.
63
+
64
+ As a last resort — for example a `require()` that can never run — set `WORKFLOW_ALLOW_UNSAFE_FLOW_BUNDLE=1` to downgrade the build failure to a warning. The bundle still fails at runtime if the code is reached.
65
+
30
66
  ## Quick fix
31
67
 
32
68
  Move any code using Node.js modules to a step function. Step functions have full Node.js runtime access.
@@ -168,7 +168,7 @@ Uncaught, the run fails immediately with the `USER_ERROR` code, without retrying
168
168
 
169
169
  On Vercel, backend connection failures and interrupted event streams use the existing retry policies, even when their error codes are unrecognized. This lets workflows recover from network failures instead of immediately failing with `USER_ERROR`. Persistent failures can still exhaust the retry budget.
170
170
 
171
- The SDK also replaces its shared events connection pool after repeated HTTP/2 session failures. Invalid backend URLs, including unsupported protocols and embedded credentials, fail immediately. Fetch requests to blocked ports or with unsupported headers (such as `Expect`) also fail without retrying. Interrupted event writes retain their existing retries; caller cancellations do not trigger another write.
171
+ The SDK also replaces its shared events connection pool after repeated HTTP/2 session failures, and retries event-log reads whose HTTP/2 stream the backend resets. An events request that receives no response data for 60 seconds fails and is retried instead of stalling the invocation. Invalid backend URLs, including unsupported protocols and embedded credentials, fail immediately. Fetch requests to blocked ports or with unsupported headers (such as `Expect`) also fail without retrying. Interrupted event writes retain their existing retries; caller cancellations do not trigger another write.
172
172
 
173
173
  A connection failure does not prove that the backend rejected a write: it may have accepted it before the response was lost. Continue to make step side effects [idempotent](/docs/foundations/idempotency).
174
174
 
@@ -206,9 +206,9 @@ try {
206
206
  | `REPLAY_TIMEOUT` | A workflow replay exceeded the maximum allowed duration |
207
207
  | `REPLAY_DIVERGENCE` | A replay could not consume the event log deterministically, usually because of non-deterministic workflow code. |
208
208
  | `CORRUPTED_EVENT_LOG` | The event log cannot be replayed: it contains orphaned or mismatched events, or one of its stored payloads is no longer readable from the World's storage. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
209
- | `STREAM_ERROR` | Workflow stream infrastructure failed while reading or writing data. This is an SDK or backend failure rather than an error in workflow code |
209
+ | `STREAM_ERROR` | Workflow stream infrastructure failed while reading or writing data. This is an SDK or backend failure rather than an error in workflow code; retry the run and report persistent failures with the `runId` |
210
210
  | `WORLD_CONTRACT_ERROR` | A World response violated the SDK contract; points at a World implementation bug |
211
- | `DEPLOYMENT_MISMATCH` | The run was delivered to a deployment other than the one it is pinned to, and automatic re-routing did not recover it |
211
+ | `DEPLOYMENT_MISMATCH` | The run was delivered to a deployment other than the one it is pinned to, and automatic re-routing did not recover it. See [deployment-mismatch](/docs/errors/deployment-mismatch) |
212
212
  | `RUNTIME_ERROR` | An internal runtime error. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
213
213
 
214
214
  <Callout type="info">
@@ -82,7 +82,7 @@ export async function POST(request: Request) {
82
82
 
83
83
  The key points:
84
84
  - Hooks allow you to pass **any [serializable data](/docs/foundations/serialization)** as the payload
85
- - You need the hook's `token` to resume it
85
+ - You need the hook's `token` to resume it, but knowing the token does not authorize the caller. Check who is calling before `resumeHook()`; see [Security](#security)
86
86
  - The workflow will resume execution right where it left off
87
87
 
88
88
  ### Checking for token conflicts
@@ -285,7 +285,7 @@ Hooks require you to manually handle HTTP requests and route them to workflows.
285
285
  When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
286
286
 
287
287
  <Callout type="warn">
288
- `createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
288
+ `createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. Make sure to read up on [security](#security) before using a webhook for calls that need to be authenticated.
289
289
  </Callout>
290
290
 
291
291
  <Callout type="info">
@@ -459,6 +459,94 @@ export async function eventCollectorWorkflow() {
459
459
  - You need to send HTTP responses back to the caller
460
460
  - You want automatic URL routing without writing API handlers
461
461
 
462
+ ## Security
463
+
464
+ A hook token tells the runtime which hook a payload belongs to. It is not an authentication mechanism, and neither `resumeHook()` nor the webhook endpoint checks who is sending the payload.
465
+
466
+ ### Generated tokens are hard to guess, not secret
467
+
468
+ When you don't pass a `token`, the SDK generates one inside the workflow function. Workflow code must produce the same values on every replay, so the generated token comes from the run's deterministic random number generator, the same one that backs [`Math.random()` and `crypto.randomUUID()`](/docs/api-reference/workflow-globals) in workflow functions. That generator is seeded from identifiers of the run, including the run ID, not from a secret key.
469
+
470
+ A generated token is hard to guess without knowing the run, but the values it is derived from are not designed to be kept secret. Treat a generated token like an unlisted link, not like a credential.
471
+
472
+ Custom tokens passed to `createHook({ token })` are usually built from domain data such as an order ID, so they are even easier to reconstruct. That is what makes them useful for routing, and it is why the route that resumes them must do its own authorization. If you can not perform your own authorization on the route that calls `resume` for any reason, and need to generate an unguessable token instead, generate it in a step where `crypto` is not seeded and pass it as `token`:
473
+
474
+ ```typescript lineNumbers
475
+ import { createHook } from "workflow";
476
+
477
+ async function generateToken() {
478
+ "use step";
479
+ // Steps run outside the workflow sandbox, so this uses the platform's
480
+ // cryptographic random source instead of the run's seed.
481
+ return crypto.randomUUID();
482
+ }
483
+
484
+ export async function approvalWorkflow() {
485
+ "use workflow";
486
+
487
+ const token = await generateToken(); // [!code highlight]
488
+ using hook = createHook<{ approved: boolean }>({ token }); // [!code highlight]
489
+
490
+ return (await hook).approved;
491
+ }
492
+ ```
493
+
494
+ The step result is recorded in the run's event log like any other step result. See [Encryption](/docs/how-it-works/encryption) to keep it encrypted at rest.
495
+
496
+ ### Webhook URLs
497
+
498
+ [`createWebhook()`](/docs/api-reference/workflow/create-webhook) serves a public route at `/.well-known/workflow/v1/webhook/:token`, and matching the token is the only check it performs. Anyone who has the URL, or can compute its token, can resume the workflow with a request of their choosing. That is fine for low-stakes callbacks and prototypes. When a webhook request triggers something consequential, either:
499
+
500
+ - Verify each request before acting on it, for example by checking the provider's HMAC signature in a step with [`respondWith: "manual"`](#dynamic-responses-manual-mode), and keep waiting for the next request when verification fails.
501
+ - Use [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the caller before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook), as shown below.
502
+
503
+ ### Authorize before calling `resumeHook()`
504
+
505
+ `resumeHook()` delivers the payload to whichever hook owns the token. The route that calls it has to authenticate the caller and check that they are allowed to resume that specific hook. Knowing the token is not proof of either. One way is to record who may resume the hook in its `metadata`, then compare it to the signed-in user:
506
+
507
+ ```typescript lineNumbers
508
+ import { createHook } from "workflow";
509
+
510
+ export async function expenseWorkflow(approverId: string) {
511
+ "use workflow";
512
+
513
+ using hook = createHook<{ approved: boolean }>({
514
+ metadata: { approverId }, // [!code highlight]
515
+ });
516
+
517
+ return (await hook).approved;
518
+ }
519
+ ```
520
+
521
+ ```typescript lineNumbers
522
+ import { getHookByToken, resumeHook } from "workflow/api";
523
+
524
+ declare function getSession(request: Request): Promise<{ userId: string } | null>; // @setup
525
+
526
+ export async function POST(request: Request) {
527
+ const session = await getSession(request); // [!code highlight]
528
+ if (!session) {
529
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
530
+ }
531
+
532
+ const { token, approved } = await request.json();
533
+ const hook = await getHookByToken(token);
534
+ const metadata = (await hook.metadata) as { approverId?: string } | undefined;
535
+ if (metadata?.approverId !== session.userId) { // [!code highlight]
536
+ return Response.json({ error: "Forbidden" }, { status: 403 });
537
+ }
538
+
539
+ await resumeHook(token, { approved });
540
+ return Response.json({ success: true });
541
+ }
542
+ ```
543
+
544
+ Take the user identity from your authentication layer, never from the request body. Other examples in these docs omit authorization to stay short.
545
+
546
+ ### Randomness in workflow functions
547
+
548
+ The same determinism applies to your own code. `Math.random()`, `crypto.randomUUID()`, and `crypto.getRandomValues()` in a workflow function return values derived from the run's seed, so they are predictable to anyone who knows it. Don't use them for secrets, passwords, one-time codes, or any value that must be unguessable. Generate those in a step, as in the token example above.
549
+
462
550
  ## Advanced patterns
463
551
 
464
552
  ### Type-safe hooks with `defineHook()`
@@ -514,7 +602,7 @@ This pattern is especially valuable in larger applications where the workflow an
514
602
 
515
603
  ### Token design
516
604
 
517
- Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always generate their own unique tokens. A generated token is not trivial to guess, but it is not a strong security contract either, so anyone who obtains the URL can invoke an unintended webhook resumption. To prevent unauthenticated run resumptions entirely, prefer a **hook** over the **webhook** convenience and implement your own authentication on the route that calls `resumeHook()`.
605
+ Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always generate their own unique tokens. Neither kind of token authorizes the sender; see [Security](#security).
518
606
 
519
607
  When using custom tokens with `createHook()`:
520
608
 
@@ -36,7 +36,7 @@ The following types can be serialized and passed through workflow functions:
36
36
  - `BigInt64Array`, `BigUint64Array`
37
37
  - `DataView`
38
38
  - `Date`
39
- - `Float32Array`, `Float64Array`
39
+ - `Float16Array`, `Float32Array`, `Float64Array`
40
40
  - `Int8Array`, `Int16Array`, `Int32Array`
41
41
  - `Map<Serializable, Serializable>`
42
42
  - `RegExp`
@@ -337,7 +337,7 @@ async function runTurn(holderRunId: string, turn: number) {
337
337
  Pass `{ namespace: "name" }` to target a [namespaced stream](#namespaced-streams). The writable can also be forwarded through `start()` and into steps.
338
338
 
339
339
  <Callout type="warn">
340
- Contributors should call `releaseLock()`, which flushes pending writes. Calling `close()` closes the shared stream for every writer.
340
+ Contributors should call `releaseLock()`, which flushes pending writes. Calling `close()` closes the shared stream for every writer. Calling `abort()` keeps the chunks already written and leaves the shared stream open.
341
341
  </Callout>
342
342
 
343
343
  The API grants append access, not read access or additional authorization. The owning run controls the stream's lifecycle, and writes to an unknown run fail.
@@ -49,7 +49,7 @@ export async function processOrderWorkflow(orderId: string) {
49
49
 
50
50
  Determinism in the workflow is required to resume the workflow from a suspension. Essentially, the workflow code gets re-run multiple times during its lifecycle, each time using the [event log](/docs/how-it-works/event-sourcing) to resume the workflow to the correct spot.
51
51
 
52
- The sandboxed environment that workflows run in already ensures determinism. For instance, `Math.random` and `Date` constructors are fixed in workflow runs, so you are safe to use them, and the framework ensures that the values don't change across replays.
52
+ The sandboxed environment that workflows run in already ensures determinism. For instance, `Math.random` and `Date` constructors are fixed in workflow runs, so you are safe to use them, and the framework ensures that the values don't change across replays. Seeded random values are not secret, so generate secrets and unguessable tokens in a step (see [Hook and webhook security](/docs/foundations/hooks#security)).
53
53
 
54
54
  ## Step functions
55
55
 
@@ -61,12 +61,12 @@ export default defineConfig({
61
61
  | --- | --- | --- | --- |
62
62
  | `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` (dev) / `false` (prod) | Controls source maps on generated workflow bundles. Accepts the same values as esbuild's `sourcemap` option. Defaults to `'inline'` in development and `false` in production (smaller function bundles, which helps stay under the Vercel 250 MB function size limit). Set it explicitly, or use the `WORKFLOW_SOURCEMAP` environment variable, to override in either environment. |
63
63
 
64
- <Accordion type="single" collapsible>
65
- <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
66
- <AccordionTrigger className="text-sm">
67
- ### Set up IntelliSense for TypeScript (optional)
68
- </AccordionTrigger>
69
- <AccordionContent className="[&_p]:my-2">
64
+ <Details>
65
+ <Summary className="[&_h3]:my-0">
66
+
67
+ ### Set up IntelliSense for TypeScript (optional)
68
+
69
+ </Summary>
70
70
 
71
71
  To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`:
72
72
 
@@ -83,9 +83,7 @@ To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.jso
83
83
  }
84
84
  ```
85
85
 
86
- </AccordionContent>
87
- </AccordionItem>
88
- </Accordion>
86
+ </Details>
89
87
 
90
88
  </Step>
91
89
 
@@ -72,12 +72,9 @@ export default defineNitroConfig({
72
72
  });
73
73
  ```
74
74
 
75
- <Accordion type="single" collapsible>
76
- <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
77
- <AccordionTrigger className="[&_p]:my-0 text-lg [&_p]:text-foreground">
78
- Setup IntelliSense for TypeScript (Optional)
79
- </AccordionTrigger>
80
- <AccordionContent className="[&_p]:my-2">
75
+ <Details>
76
+ <Summary>Setup IntelliSense for TypeScript (Optional)</Summary>
77
+
81
78
  To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`:
82
79
 
83
80
  ```json title="tsconfig.json" lineNumbers
@@ -93,10 +90,7 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json
93
90
  }
94
91
  ```
95
92
 
96
- </AccordionContent>
97
-
98
- </AccordionItem>
99
- </Accordion>
93
+ </Details>
100
94
 
101
95
  ### Update `package.json`
102
96
 
@@ -72,12 +72,9 @@ export default defineNitroConfig({
72
72
  });
73
73
  ```
74
74
 
75
- <Accordion type="single" collapsible>
76
- <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
77
- <AccordionTrigger className="[&_p]:my-0 text-lg [&_p]:text-foreground">
78
- Setup IntelliSense for TypeScript (Optional)
79
- </AccordionTrigger>
80
- <AccordionContent className="[&_p]:my-2">
75
+ <Details>
76
+ <Summary>Setup IntelliSense for TypeScript (Optional)</Summary>
77
+
81
78
  To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.json`:
82
79
 
83
80
  ```json title="tsconfig.json" lineNumbers
@@ -93,9 +90,7 @@ To enable helpful hints in your IDE, set up the workflow plugin in `tsconfig.jso
93
90
  }
94
91
  ```
95
92
 
96
- </AccordionContent>
97
- </AccordionItem>
98
- </Accordion>
93
+ </Details>
99
94
 
100
95
  ### Update `package.json`
101
96
 
@@ -55,12 +55,9 @@ export default defineConfig({
55
55
  });
56
56
  ```
57
57
 
58
- <Accordion type="single" collapsible>
59
- <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
60
- <AccordionTrigger className="[&_p]:my-0 text-lg [&_p]:text-foreground">
61
- Setup IntelliSense for TypeScript (Optional)
62
- </AccordionTrigger>
63
- <AccordionContent className="[&_p]:my-2">
58
+ <Details>
59
+ <Summary>Setup IntelliSense for TypeScript (Optional)</Summary>
60
+
64
61
  To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`:
65
62
 
66
63
  ```json title="tsconfig.json" lineNumbers
@@ -76,10 +73,7 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json
76
73
  }
77
74
  ```
78
75
 
79
- </AccordionContent>
80
-
81
- </AccordionItem>
82
- </Accordion>
76
+ </Details>
83
77
 
84
78
  ### Update `package.json`
85
79
 
@@ -80,14 +80,14 @@ import { SiReactrouter } from "@icons-pack/react-simple-icons";
80
80
  <div className="flex flex-col items-center justify-center gap-2">
81
81
  <Python className="size-16" />
82
82
  <span className="font-medium">Python</span>
83
- <Badge variant="secondary">Beta</Badge>
83
+ <Badge variant="gray" size="sm">Beta</Badge>
84
84
  </div>
85
85
  </Card>
86
86
  <Card href="/docs/getting-started/nestjs">
87
87
  <div className="flex flex-col items-center justify-center gap-2">
88
88
  <Nest className="size-16 dark:invert" />
89
89
  <span className="font-medium">NestJS</span>
90
- <Badge variant="secondary">Experimental</Badge>
90
+ <Badge variant="secondary">Beta</Badge>
91
91
  </div>
92
92
  </Card>
93
93
  </Cards>