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
@@ -140,7 +140,7 @@ Supply a unique, deterministic approval token from the caller. The workflow must
140
140
 
141
141
  `start()` validates the source before it writes anything, so a definition that could never run fails at the call site rather than on a queue delivery:
142
142
 
143
- - It must declare `async function workflow(...)`. Pass `experimental_dynamic.exportName` to use a different name; export names may contain letters, digits, and `_`, and cannot start with a digit.
143
+ - It must declare `async function workflow(...)`. Pass `experimental_dynamic.exportName` to use a different name; export names may contain letters, digits, and `_`, cannot start with a digit, and are at most 64 characters.
144
144
  - The function's first statement must be the `"use workflow"` directive.
145
145
  - No `import` or `export`. Reach steps through `steps`, not through modules.
146
146
  - JavaScript only — no TypeScript syntax, no npm dependencies, no bundling.
@@ -182,6 +182,8 @@ Both paths are transparent — there is nothing to configure. Here, “inline”
182
182
 
183
183
  Alongside the serialized code, the run records small plaintext metadata on `executionContext.dynamicWorkflow`: the source hash, the export name, and the alias-to-step-ID map. That is what lets a run be identified as dynamic without decoding the source. It is plaintext even when the code is encrypted, so anyone who can read the run can see which step IDs it was given and the aliases they were given under.
184
184
 
185
+ In the [observability UI](/docs/observability), a dynamic run's detail view shows its stored code in a **Workflow Code** section, behind the same decrypt action as the run's input and output. **Replay Run** is unavailable for dynamic runs. To run the definition again, call `start()` with the same source.
186
+
185
187
  ## World support
186
188
 
187
189
  Dynamic workflows need a World that can store the run's workflow code.
@@ -219,6 +221,7 @@ Treat dynamic source the way you would treat code in a pull request: written or
219
221
  - Steps must already be registered in the deployment; no runtime step registration.
220
222
  - No inline `"use step"` functions, `createWebhook`, or `getWritable`.
221
223
  - No caller-provided workflow IDs.
224
+ - No **Replay Run** from the observability UI.
222
225
  - Parser-based validation checks JavaScript syntax and the required source/wrapper shape without executing it. It does not validate behavior, determinism, or intent.
223
226
  - On Vercel, roughly 30 step aliases fit the 2,048-byte execution-context limit.
224
227
  - Requires a World with dynamic-source storage.
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Advanced
3
+ description: Features for workflows that go beyond the build-time execution model.
4
+ type: overview
5
+ summary: Explore features for cases that workflows compiled into your build do not cover.
6
+ related:
7
+ - /docs/foundations
8
+ - /docs/how-it-works/code-transform
9
+ ---
10
+
11
+ These features build on the [foundations](/docs/foundations) for cases that workflows compiled into your build do not cover, such as orchestration whose shape is only known after you deploy.
12
+
13
+ <AutoCards />
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "Advanced",
3
+ "pages": ["dynamic-workflows"],
4
+ "defaultOpen": false
5
+ }
@@ -267,9 +267,9 @@ Use the multi-turn pattern when:
267
267
 
268
268
  The multi-turn pattern also supports messages from system events, external services, and multiple users. Every source resumes the same Hook; the workflow queues those messages and processes them between model turns.
269
269
 
270
- <Tabs items={['System event', 'External service', 'Multiple users']}>
270
+ <TabsWithChildren tabs={["System event","External service","Multiple users"]}>
271
271
 
272
- <Tab value="System event">
272
+ <TabContent order={1}>
273
273
 
274
274
  Scheduled tasks, background jobs, or database triggers can inject updates into an active conversation:
275
275
 
@@ -287,9 +287,9 @@ export async function POST(request: Request) {
287
287
  }
288
288
  ```
289
289
 
290
- </Tab>
290
+ </TabContent>
291
291
 
292
- <Tab value="External service">
292
+ <TabContent order={2}>
293
293
 
294
294
  A third-party webhook can notify the conversation about an external event:
295
295
 
@@ -309,9 +309,9 @@ export async function POST(request: Request) {
309
309
  }
310
310
  ```
311
311
 
312
- </Tab>
312
+ </TabContent>
313
313
 
314
- <Tab value="Multiple users">
314
+ <TabContent order={3}>
315
315
 
316
316
  Multiple authenticated users can participate in the same workflow-owned session. Include attribution when resuming the Hook:
317
317
 
@@ -337,9 +337,9 @@ export async function POST(
337
337
 
338
338
  To preserve structured attribution across refreshes, persist the corresponding `UIMessage` using the application-history approach described above.
339
339
 
340
- </Tab>
340
+ </TabContent>
341
341
 
342
- </Tabs>
342
+ </TabsWithChildren>
343
343
 
344
344
  ## Related documentation
345
345
 
@@ -154,6 +154,10 @@ export async function POST(request: Request) {
154
154
  }
155
155
  ```
156
156
 
157
+ <Callout type="warn">
158
+ This route resumes whichever hook owns `toolCallId`, so anyone who learns the ID can submit a decision. In production, authenticate the request and check that the signed-in user may approve this booking before calling `resume()`. See [Hook and webhook security](/docs/foundations/hooks#security).
159
+ </Callout>
160
+
157
161
  </Step>
158
162
 
159
163
  <Step>
@@ -185,7 +189,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr
185
189
  if (output) {
186
190
  return (
187
191
  <div className="border rounded-lg p-4">
188
- <p className="text-sm text-muted-foreground">{output}</p>
192
+ <p className="text-sm text-gray-900">{output}</p>
189
193
  </div>
190
194
  );
191
195
  }
@@ -207,7 +211,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr
207
211
  <div className="border rounded-lg p-4 space-y-4">
208
212
  <div className="space-y-2">
209
213
  <p className="font-medium">Approve this booking?</p>
210
- <div className="text-sm text-muted-foreground">
214
+ <div className="text-sm text-gray-900">
211
215
  {input && (
212
216
  <div className="space-y-2">
213
217
  <div>Flight: {input.flightNumber}</div>
@@ -334,7 +338,7 @@ export default function ChatPage() {
334
338
 
335
339
  ## Using webhooks directly
336
340
 
337
- For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow:
341
+ For simpler cases where you don't need type-safe validation or programmatic resumption, you can use [`createWebhook()`](/docs/api-reference/workflow/create-webhook) directly. This generates a unique URL that can be called to resume the workflow. Anyone with the URL can resume it, so avoid this for approvals that need to know who approved, unless you use the incoming payload for authorization. See [Hook and webhook security](/docs/foundations/hooks#security).
338
342
 
339
343
  ```typescript title="workflows/chat/steps/tools.ts" lineNumbers
340
344
  import { createWebhook } from "workflow";
package/docs/ai/index.mdx CHANGED
@@ -59,9 +59,9 @@ cd workflow-examples/flight-booking-app
59
59
 
60
60
  ### Configure model access
61
61
 
62
- <Tabs items={['AI Gateway', 'Provider package']}>
62
+ <TabsWithChildren tabs={["AI Gateway","Provider package"]}>
63
63
 
64
- <Tab value="AI Gateway">
64
+ <TabContent order={1}>
65
65
 
66
66
  AI SDK uses [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) as its default global provider, so plain `"provider/model"` strings need no provider-specific package. Vercel deployments authenticate with OIDC automatically. For local development, link the project and pull a short-lived OIDC token:
67
67
 
@@ -72,9 +72,9 @@ vercel env pull .env.local
72
72
 
73
73
  You can alternatively set `AI_GATEWAY_API_KEY` from the [AI Gateway authentication](https://vercel.com/docs/ai-gateway/authentication) page.
74
74
 
75
- </Tab>
75
+ </TabContent>
76
76
 
77
- <Tab value="Provider package">
77
+ <TabContent order={2}>
78
78
 
79
79
  `WorkflowAgent` accepts any AI SDK provider. To use OpenAI, install its provider package:
80
80
 
@@ -98,9 +98,9 @@ const model = openai("gpt-5.6-sol");
98
98
 
99
99
  See the [AI SDK provider guide](https://ai-sdk.dev/providers/ai-sdk-providers) for Anthropic, Google, Amazon Bedrock, and other providers.
100
100
 
101
- </Tab>
101
+ </TabContent>
102
102
 
103
- </Tabs>
103
+ </TabsWithChildren>
104
104
  </Step>
105
105
 
106
106
  <Step>
@@ -111,9 +111,9 @@ Run the app with `npm run dev` and open [http://localhost:3000](http://localhost
111
111
 
112
112
  The following sections break down the core code. You don't need to make changes yet.
113
113
 
114
- <Tabs items={['API Route', 'Tools', 'Client']}>
114
+ <TabsWithChildren tabs={["API Route","Tools","Client"]}>
115
115
 
116
- <Tab value="API Route">
116
+ <TabContent order={1}>
117
117
 
118
118
  Our API route calls [AI SDK's `ToolLoopAgent` class](https://ai-sdk.dev/docs/agents/overview), which encapsulates the LLM call, tool execution loop, and stopping conditions on top of [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.
119
119
 
@@ -137,9 +137,9 @@ export async function POST(req: Request) {
137
137
  }
138
138
  ```
139
139
 
140
- </Tab>
140
+ </TabContent>
141
141
 
142
- <Tab value="Tools">
142
+ <TabContent order={2}>
143
143
 
144
144
  Our tools are mostly mocked out for the sake of the example. We use AI SDK's `tool` function to define the tool, and pass it to the agent. In your own app, this might be any kind of tool call, like database queries, calls to external services, etc.
145
145
 
@@ -160,9 +160,9 @@ async function searchFlights({ from, to, date }: { from: string; to: string; dat
160
160
  }
161
161
  ```
162
162
 
163
- </Tab>
163
+ </TabContent>
164
164
 
165
- <Tab value="Client">
165
+ <TabContent order={3}>
166
166
 
167
167
  Our `ChatPage` component contains logic for displaying chat messages, but its core responsibility is managing input and output for the [`useChat` hook](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat#usechat) from AI SDK.
168
168
 
@@ -207,9 +207,9 @@ export default function ChatPage() {
207
207
  }
208
208
  ```
209
209
 
210
- </Tab>
210
+ </TabContent>
211
211
 
212
- </Tabs>
212
+ </TabsWithChildren>
213
213
 
214
214
  </Step>
215
215
 
@@ -120,9 +120,9 @@ Update your chat component to detect and render the custom data parts. Data part
120
120
  to: string; // [!code highlight]
121
121
  }; // [!code highlight]
122
122
  return ( // [!code highlight]
123
- <div key={`${part.id}-${flight.flightNumber}`} className="p-3 bg-muted rounded-md"> // [!code highlight]
123
+ <div key={`${part.id}-${flight.flightNumber}`} className="p-3 bg-gray-100 rounded-md"> // [!code highlight]
124
124
  <div className="font-medium">{flight.airline} - {flight.flightNumber}</div> // [!code highlight]
125
- <div className="text-muted-foreground">{flight.from} → {flight.to}</div> // [!code highlight]
125
+ <div className="text-gray-900">{flight.from} → {flight.to}</div> // [!code highlight]
126
126
  </div> // [!code highlight]
127
127
  ); // [!code highlight]
128
128
  } // [!code highlight]
@@ -8,11 +8,11 @@ The `@workflow/vitest` package provides a Vitest plugin and test helpers for run
8
8
  ## Installation
9
9
 
10
10
  ```package-install
11
- npm i -D @workflow/vitest@beta
11
+ npm i -D @workflow/vitest
12
12
  ```
13
13
 
14
14
  <Callout type="warn">
15
- `@workflow/vitest@latest` is still the 4.x line, so a Workflow 5 app has to install the `beta` tag (or pin the matching beta, for example `@workflow/vitest@5.0.0-beta.53`). The package carries its own copy of `@workflow/core` and runs your workflows against it, so it has to move with `workflow`.
15
+ The package carries its own copy of `@workflow/core` and runs your workflows against it, so it has to move with `workflow` and stay on the same major.
16
16
 
17
17
  `globalSetup` compares the two copies once per run: a different major fails the run with the install command that fixes it, and any other difference logs a warning. Set [`WORKFLOW_VITEST_VERSION_CHECK=off`](/docs/configuration/build-and-diagnostics#workflow_vitest_version_check) to skip the check.
18
18
  </Callout>
@@ -15,6 +15,10 @@ Creates a low-level hook primitive that can be used to resume a workflow run wit
15
15
 
16
16
  Hooks allow external systems to send data to a paused workflow without the HTTP-specific constraints of webhooks. They're identified by a token and can receive any serializable payload.
17
17
 
18
+ <Callout type="warn">
19
+ A hook token routes a payload to the right hook; it does not authorize the sender. Generated tokens are hard to guess but are not secrets, and custom tokens are usually easy to reconstruct. Authorize callers in the route that calls [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook). See [Hook and webhook security](/docs/foundations/hooks#security).
20
+ </Callout>
21
+
18
22
  ```ts lineNumbers
19
23
  import { createHook } from "workflow"
20
24
 
@@ -14,7 +14,7 @@ Creates a webhook that can be used to suspend and resume a workflow run upon rec
14
14
  Webhooks provide a way for external systems to send HTTP requests directly to your workflow. Unlike hooks which accept arbitrary payloads, webhooks work with standard HTTP `Request` objects and can return HTTP `Response` objects.
15
15
 
16
16
  <Callout type="warn">
17
- `createWebhook()` creates a public endpoint at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. This is convenient for prototypes and basic resume links because it avoids 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.
17
+ `createWebhook()` creates a public endpoint at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests resuming that webhook. Anyone who has the URL can resume the workflow with a request of their choosing, so verify requests before acting on them or use [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own authorized route. See [Hook and webhook security](/docs/foundations/hooks#security).
18
18
  </Callout>
19
19
 
20
20
  ```ts lineNumbers
@@ -17,6 +17,10 @@ This is a lightweight wrapper around [`createHook()`](/docs/api-reference/workfl
17
17
  We recommend using `defineHook()` over `createHook()` in production codebases for better type safety and optional runtime validation.
18
18
  </Callout>
19
19
 
20
+ <Callout type="warn">
21
+ Schema validation checks the payload's shape, not who sent it. Authorize callers before calling `resume()`. See [Hook and webhook security](/docs/foundations/hooks#security).
22
+ </Callout>
23
+
20
24
  ```ts lineNumbers
21
25
  import { defineHook } from "workflow";
22
26
 
@@ -93,13 +93,19 @@ export async function POST(request: Request) {
93
93
 
94
94
  ### Validating hook before resume
95
95
 
96
- Use `getHookByToken` to validate hook ownership or metadata before resuming:
96
+ Use `getHookByToken` to validate hook ownership or metadata before resuming. Take the user's identity from your authentication layer, not from the request body. See [Hook and webhook security](/docs/foundations/hooks#security).
97
97
 
98
98
  ```typescript lineNumbers
99
99
  import { getHookByToken, resumeHook } from "workflow/api";
100
100
 
101
+ declare function getSession(request: Request): Promise<{ userId: string } | null>; // @setup
102
+
101
103
  export async function POST(request: Request) {
102
- const { token, userId, data } = await request.json();
104
+ const session = await getSession(request);
105
+ if (!session) {
106
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
107
+ }
108
+ const { token, data } = await request.json();
103
109
 
104
110
  try {
105
111
  const hook = await getHookByToken(token); // [!code highlight]
@@ -107,7 +113,7 @@ export async function POST(request: Request) {
107
113
  const metadata = (await hook.metadata) as { allowedUserId?: string } | undefined; // [!code highlight]
108
114
 
109
115
  // Validate that the hook metadata matches the user
110
- if (metadata?.allowedUserId !== userId) {
116
+ if (metadata?.allowedUserId !== session.userId) {
111
117
  return Response.json(
112
118
  { error: "Unauthorized to resume this hook" },
113
119
  { status: 403 }
@@ -22,6 +22,10 @@ If `resumeHook()` throws any other error, the outcome is ambiguous only in dispa
22
22
  `resumeHook` is a runtime function that must be called from outside a workflow function.
23
23
  </Callout>
24
24
 
25
+ <Callout type="warn">
26
+ `resumeHook()` does not check who is calling it. Authenticate the caller and confirm they may resume this hook before calling it; knowing the token is not enough. The examples below omit that check for brevity. See [Hook and webhook security](/docs/foundations/hooks#security).
27
+ </Callout>
28
+
25
29
  ```typescript lineNumbers
26
30
  import { resumeHook } from "workflow/api";
27
31
 
@@ -17,6 +17,10 @@ This function publishes a workflow invocation carrying the request; the runtime
17
17
  `resumeWebhook` is a runtime function that must be called from outside a workflow function.
18
18
  </Callout>
19
19
 
20
+ <Callout type="warn">
21
+ The webhook token is the only authorization `resumeWebhook()` and the public webhook route perform. Verify requests before acting on them. See [Hook and webhook security](/docs/foundations/hooks#security).
22
+ </Callout>
23
+
20
24
  ```typescript lineNumbers
21
25
  import { resumeWebhook } from "workflow/api";
22
26
 
@@ -32,6 +32,10 @@ These APIs are available but are **seeded or fixed** to ensure deterministic beh
32
32
  You can safely use `Math.random()`, `Date.now()`, and `crypto.randomUUID()` in workflow functions. The framework ensures these return the same values across replays.
33
33
  </Callout>
34
34
 
35
+ <Callout type="warn">
36
+ `Math.random()`, `crypto.randomUUID()`, and `crypto.getRandomValues()` are derived from the run's seed rather than a secret, so their values are predictable to anyone who knows that seed. Don't use them for secrets, one-time codes, or tokens that must be unguessable; generate those in a step instead. See [Hook and webhook security](/docs/foundations/hooks#security).
37
+ </Callout>
38
+
35
39
  ## Web platform APIs
36
40
 
37
41
  These standard Web APIs are available in workflow functions:
@@ -10,7 +10,7 @@ related:
10
10
  NestJS integration for Workflow SDK. The `WorkflowModule` builds the workflow bundles on application startup and registers the controller that serves the workflow runtime routes.
11
11
 
12
12
  <Callout>
13
- NestJS integration is experimental and not yet supported for deployment to Vercel. The same exports are also available from the `@workflow/nest` package.
13
+ NestJS integration is in beta. Both `@nestjs/platform-express` and `@nestjs/platform-fastify` are supported, and deploying to Vercel goes through [`workflow-nest build --vercel`](/docs/getting-started/nestjs#deploy-to-vercel). The same exports are also available from the `@workflow/nest` package.
14
14
  </Callout>
15
15
 
16
16
  ## Exports
@@ -25,6 +25,9 @@ NestJS integration is experimental and not yet supported for deployment to Verce
25
25
  <Card title="WorkflowController" href="/docs/api-reference/workflow-nest/workflow-controller">
26
26
  Controller that serves the workflow runtime routes under `.well-known/workflow/v1`
27
27
  </Card>
28
+ <Card title="isWorkflowRequest()" href="/docs/api-reference/workflow-nest/is-workflow-request">
29
+ Recognises a workflow request in a guard, so queue deliveries and webhooks are not rejected by your own auth
30
+ </Card>
28
31
  <Card title="configureWorkflowController()" href="/docs/api-reference/workflow-nest/configure-workflow-controller">
29
32
  Points `WorkflowController` at the directory containing the generated workflow bundles
30
33
  </Card>
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: isWorkflowRequest
3
+ description: Recognise a Workflow SDK request inside a NestJS guard or interceptor.
4
+ type: reference
5
+ summary: Let queue deliveries and webhooks past an application guard.
6
+ prerequisites:
7
+ - /docs/getting-started/nestjs
8
+ ---
9
+
10
+ Returns `true` when NestJS routed the request an `ExecutionContext` is handling to [`WorkflowController`](/docs/api-reference/workflow-nest/workflow-controller), which serves the Workflow SDK's protocol routes under `.well-known/workflow/v1`. The check is on the selected controller rather than the URL, so an application route that can be reached through a URL containing `.well-known/workflow/v1` (a wildcard such as `files/*path`) is still treated as your route.
11
+
12
+ [`WorkflowController`](/docs/api-reference/workflow-nest/workflow-controller) is a controller inside your application, so a global guard runs for it too. A guard that rejects unauthenticated requests rejects every queue delivery and webhook with `403`, and runs stop making progress with no other symptom. Use this helper to exempt them.
13
+
14
+ The workflow routes authenticate their own callers — queue deliveries are signed and webhook tokens are single-use secrets — so letting them past an application guard exposes nothing.
15
+
16
+ ## Usage
17
+
18
+ {/* @skip-typecheck - NestJS decorators require special TypeScript config */}
19
+
20
+ ```typescript title="src/auth.guard.ts" lineNumbers
21
+ import {
22
+ Injectable,
23
+ type CanActivate,
24
+ type ExecutionContext,
25
+ } from "@nestjs/common";
26
+ import { isWorkflowRequest } from "workflow/nest"; // [!code highlight]
27
+
28
+ @Injectable()
29
+ export class AuthGuard implements CanActivate {
30
+ canActivate(context: ExecutionContext) {
31
+ if (isWorkflowRequest(context)) return true; // [!code highlight]
32
+ return this.authenticate(context);
33
+ }
34
+ }
35
+ ```
36
+
37
+ ## API signature
38
+
39
+ ### Parameters
40
+
41
+ | Parameter | Type | Description |
42
+ | --- | --- | --- |
43
+ | `context` | `ExecutionContext` | The execution context NestJS passes to a guard or interceptor. |
44
+
45
+ ### Returns
46
+
47
+ `boolean`. `false` for a request to any other route, and for non-HTTP execution contexts (RPC, WebSockets), where there is no workflow route to match.
48
+
49
+ ## Related exports
50
+
51
+ | Export | Description |
52
+ | --- | --- |
53
+ | `isWorkflowRoutePath(path, globalPrefix?)` | Whether a raw URL or path addresses a workflow route, for middleware that has a request rather than an `ExecutionContext`. The match is anchored to `globalPrefix` (default `''`), so pass the prefix given to `app.setGlobalPrefix()` unless it excludes the workflow routes. |
54
+ | `WORKFLOW_ROUTE_PREFIX` | `'.well-known/workflow/v1'`, the path the controller is mounted at. |
55
+
56
+ <Callout type="info">
57
+ Interceptors and exception filters need no exemption. The workflow handlers write through `@Res()`, so the exact status and body the workflow runtime produced reach the caller, which is what the queue and third-party webhook senders key off.
58
+ </Callout>
@@ -4,6 +4,7 @@
4
4
  "workflow-module",
5
5
  "nest-local-builder",
6
6
  "workflow-controller",
7
+ "is-workflow-request",
7
8
  "configure-workflow-controller"
8
9
  ]
9
10
  }
@@ -9,9 +9,13 @@ prerequisites:
9
9
 
10
10
  NestJS controller that handles the well-known workflow endpoints under `.well-known/workflow/v1`. It dynamically imports the generated workflow bundles and converts between Express/Fastify requests and the Web API `Request`/`Response` objects the workflow runtime expects. Both the Express and Fastify HTTP adapters are supported.
11
11
 
12
- The conversion preserves bytes in both directions: request bodies come from `req.rawBody` when the app is created with `{ rawBody: true }`, from a `Buffer`/string body left by a parser, or read directly from the request stream when no parser claimed the content type. Responses are written as bytes, and every `set-cookie` value is kept. See [Raw request bodies](/docs/getting-started/nestjs#raw-request-bodies).
12
+ The conversion preserves bytes in both directions. [`WorkflowModule`](/docs/api-reference/workflow-nest/workflow-module) keeps the application's body parsers off these routes, so the request body is normally read straight from the stream; a `req.rawBody` left by `{ rawBody: true }`, or a `Buffer`/string body left by a parser, is used when one is present. Responses are written as bytes, and every `set-cookie` value is kept. See [Request bodies and body parsers](/docs/getting-started/nestjs#request-bodies-and-body-parsers).
13
13
 
14
- Handlers take `@Res()`, so the application's interceptors and exception filters do not wrap these routes. That is deliberate: the queue and third-party webhook senders key off the exact status and body the workflow runtime produces.
14
+ Handlers take `@Res()` and write the response themselves, so an interceptor or exception filter cannot reshape it. That is deliberate: the queue and third-party webhook senders key off the exact status and body the workflow runtime produces.
15
+
16
+ Guards do run, because NestJS runs them before the handler. A global guard that rejects unauthenticated requests rejects queue deliveries too; see [`isWorkflowRequest`](/docs/api-reference/workflow-nest/is-workflow-request).
17
+
18
+ The controller is registered as `VERSION_NEUTRAL`, so `app.enableVersioning()` does not move these routes away from the paths the SDK generates URLs for. A global prefix does apply, and `WorkflowModule` adopts it automatically.
15
19
 
16
20
  [`WorkflowModule.forRoot()`](/docs/api-reference/workflow-nest/workflow-module) registers this controller automatically. You only register it yourself if you are not using `WorkflowModule`.
17
21
 
@@ -86,6 +86,7 @@ Extends [`NestBuilderOptions`](/docs/api-reference/workflow-nest/nest-local-buil
86
86
  | `basePath` | `string` | adopted from `app.setGlobalPrefix()` | Route prefix the workflow endpoints are served under, applied to generated callback and webhook URLs. Set it when a reverse proxy mounts the app on a sub-path NestJS cannot see. |
87
87
  | `manageWorldLifecycle` | `boolean` | `false` | Start the target World's background workers with the app and close them on shutdown. Required for self-hosted Worlds, which otherwise never pick up runs. |
88
88
  | `preloadBundles` | `boolean` | `false` when `VERCEL` is set, else `true` | Load the generated bundles during startup instead of on the first request. On Vercel, dedicated functions serve the bundles, so there is nothing to preload. |
89
+ | `bypassBodyParser` | `boolean` | `true` | Keep the application's body parsers away from `.well-known/workflow/v1`, so queue deliveries are not rejected by Express's 100 KB limit and signed webhook bodies stay byte-exact. The application's own routes are untouched. On Fastify, where the body limit is enforced per instance rather than per route, nothing is patched and a limit low enough to reject deliveries is reported at startup instead. |
89
90
  | `workingDir` | `string` | `process.cwd()` | Working directory for the NestJS application. |
90
91
  | `dirs` | `string[]` | `['src']` | Directories to scan for workflow files. |
91
92
  | `outDir` | `string` | `'.nestjs/workflow'` (relative to `workingDir`) | Output directory for generated workflow bundles. |
@@ -126,9 +127,16 @@ The older `WORKFLOW_OPTIONS` token resolves to the same value and is kept for co
126
127
 
127
128
  | Hook | Behaviour |
128
129
  | --- | --- |
129
- | `onModuleInit` | Reconciles the base path against `app.setGlobalPrefix()`, builds the bundles (or verifies they exist when `skipBuild` is set), optionally starts the World, and preloads the bundles. |
130
+ | `onModuleInit` | Takes the application's body parsers off the workflow routes (unless `bypassBodyParser` is `false`), reconciles the base path against `app.setGlobalPrefix()`, builds the bundles (or verifies they exist when `skipBuild` is set), optionally starts the World, and preloads the bundles. |
130
131
  | `onApplicationShutdown` | Closes the World when `manageWorldLifecycle` is set. Call `app.enableShutdownHooks()` so this runs on a signal. |
131
132
 
133
+ <Callout type="warn">
134
+ `WorkflowModule` reads NestJS's global prefix and HTTP adapter through the
135
+ injector, which only works while a single copy of `@nestjs/core` is installed.
136
+ With two copies the injection tokens differ, prefix handling and the
137
+ body-parser bypass both stop working, and a warning is logged at startup.
138
+ </Callout>
139
+
132
140
  <Callout type="warn">
133
141
  Workflows and steps run outside the NestJS injector, so providers cannot be injected into `"use workflow"` or `"use step"` code. See [NestJS dependency injection is not available in workflows and steps](/docs/getting-started/nestjs#nestjs-dependency-injection-is-not-available-in-workflows-and-steps).
134
142
  </Callout>
@@ -204,6 +204,35 @@ const result = await world.runs.list({ // [!code highlight]
204
204
 
205
205
  **Returns:** `{ data: WorkflowRun[], cursor?: string }`
206
206
 
207
+ The array form of `status` lets you express set filters without restating the
208
+ status vocabulary. `@workflow/world` exports
209
+ `TERMINAL_WORKFLOW_RUN_STATUSES` (`['completed', 'failed', 'cancelled']`) for
210
+ that purpose:
211
+
212
+ ```typescript lineNumbers
213
+ import { TERMINAL_WORKFLOW_RUN_STATUSES } from "@workflow/world";
214
+
215
+ // Every run that has not reached a terminal status
216
+ const inFlight = await world.runs.list({
217
+ status: ["pending", "running"],
218
+ });
219
+
220
+ // The complement, without hardcoding the list
221
+ const finished = await world.runs.list({
222
+ status: [...TERMINAL_WORKFLOW_RUN_STATUSES],
223
+ });
224
+ ```
225
+
226
+ <Callout type="warn">
227
+ `status: []` matches **no** runs, mirroring SQL `IN ()`. To leave the filter
228
+ unset, omit the field entirely.
229
+
230
+ Support for the array form is per World. The Local and Postgres Worlds accept
231
+ it; the Vercel World's `/v2/runs` endpoint takes a single status today and
232
+ throws a `WorkflowWorldError` (`INVALID_ARGUMENT`) when given an array, rather
233
+ than silently filtering on something else.
234
+ </Callout>
235
+
207
236
  <Callout type="warn">
208
237
  Observability and inspection usage of `world.runs.list()` is deprecated. Use
209
238
  [`world.analytics.runs.list()`](/docs/api-reference/workflow-runtime/world/analytics#runslist)
@@ -76,4 +76,14 @@ Accepted values:
76
76
  - Dev-mode only (`next dev`). Comma-separated list of path fragments the file watcher should never watch, in addition to the built-in ignores and your project's `.gitignore`.
77
77
  - Each entry is matched as a substring of the absolute path (for example, `/fixtures/,/generated/`).
78
78
  - The watcher already respects `.gitignore` (walking from the app directory up to the workspace root). Use this variable only for large directories you cannot or do not want to add to `.gitignore`.
79
- - Useful when a project has thousands of non-ignored directories and `next dev` fails with `EMFILE: too many open files, watch`.
79
+
80
+ #### What the dev watcher tracks
81
+
82
+ In `next dev`, Workflow watches the modules your app actually imports, not your whole project:
83
+
84
+ - Every file the workflow build reached from your Next.js entrypoints. Editing one rebuilds the affected workflow bundles.
85
+ - The directories those files live in, so a module you add under an import you have already written is picked up.
86
+ - The paths behind imports that do not resolve yet. Write `import './billing/workflow'` before creating the file, or delete a workflow module and restore it, and the module is bundled as soon as it exists, wherever it lives.
87
+ - The `app` and `pages` directories (`src/` variants included), so a route you create is noticed even though nothing imports it yet, along with root entrypoints such as `middleware.ts` and `instrumentation.ts`.
88
+
89
+ A directory your app never imports is not watched at all, so files there never produce workflow bundles or rebuilds. Import one from a page and the next rebuild brings it into the watched set. Files are only ever watched through the directory that contains them, so the watcher's cost scales with the number of watched directories rather than the number of source files.
@@ -126,6 +126,12 @@ Configure Workflow through `WorkflowModule.forRoot()`.
126
126
  - Default: `false`
127
127
  - Skips bundle generation when bundles are already pre-built.
128
128
 
129
+ ### `bypassBodyParser`
130
+
131
+ - Environment override: none
132
+ - Default: `true`
133
+ - Keeps the application's body parsers away from `.well-known/workflow/v1`, so queue deliveries are not rejected by Express's 100 KB limit and signed webhook bodies stay byte-exact. Other routes are untouched. On Fastify, where the body limit is enforced per instance rather than per route, nothing is patched and a limit low enough to reject deliveries is reported at startup instead.
134
+
129
135
  ## Astro
130
136
 
131
137
  ### `sourcemap`
@@ -220,7 +220,7 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
220
220
  - Values: `node` or `quickjs`
221
221
  - Selects the sandboxed VM engine that executes workflow functions (`"use workflow"`). Step functions are unaffected and always run with full Node.js access.
222
222
  - `node` (default) runs workflow code in a [`node:vm`](https://nodejs.org/api/vm.html) context.
223
- - `quickjs` (experimental) runs workflow code in a [QuickJS](https://github.com/quickjs-ng/quickjs) VM compiled to WebAssembly (via [`quickjs-wasi`](https://github.com/vercel-labs/quickjs-wasi)). Both engines implement the same event-replay execution model (seeded PRNG, deterministic clock, and correlation-ID sequences are identical), but the **global surface is not identical**. Review the differences below before switching an existing deployment. The QuickJS engine is intended for platforms that do not implement `node:vm`, and is the foundation for future VM-memory snapshotting.
223
+ - `quickjs` (experimental) runs workflow code in a [QuickJS](https://github.com/quickjs-ng/quickjs) VM compiled to WebAssembly (via [`quickjs-wasi`](https://github.com/vercel-labs/quickjs-wasi)). Both engines implement the same event-replay execution model (seeded PRNG, deterministic clock, and correlation-ID sequences are identical), but the **global surface is not identical**. Review the differences below before switching an existing deployment. The QuickJS engine is intended for platforms that do not implement `node:vm`, and supports VM-memory snapshotting (see [`WORKFLOW_SNAPSHOT_THRESHOLD`](#workflow_snapshot_threshold)).
224
224
  - Global-surface differences under `quickjs` apply to workflow functions only. Step functions always have full Node.js:
225
225
  - `crypto.getRandomValues()` and `crypto.randomUUID()` are provided and deterministic (seeded like the node engine's). All `crypto.subtle.*` methods, including `digest`, throw with guidance to move to a step function. The node engine supports `digest`.
226
226
  - `Intl` is not available (QuickJS has no ICU). The `Intl.*` constructors throw, and `toLocaleString`-family methods (including `localeCompare`) throw when called **with an explicit locale**. Calling them without arguments keeps the engine default. Perform locale-sensitive formatting in a step function.
@@ -237,6 +237,31 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
237
237
  - A bundle whose module scope consumes randomness, reads the clock, or replaces a serialization intrinsic cannot be snapshotted safely. The runtime detects these cases when preparing the snapshot and falls back to per-invocation evaluation.
238
238
  - Set `0` or `false` to always evaluate the bundle per invocation.
239
239
 
240
+ ### `WORKFLOW_SNAPSHOT_THRESHOLD`
241
+
242
+ - Default: `0` (disabled)
243
+ - Values: non-negative integer
244
+ - Experimental. Only used by the QuickJS engine (`WORKFLOW_VM=quickjs`), and only with a World that provides snapshot storage (world-local, world-postgres, and world-vercel do).
245
+ - When set above `0`, the runtime persists a **VM-memory snapshot** at a suspension once at least this many events have been processed since the last snapshot. Subsequent invocations restore the VM from the snapshot and replay only the events recorded since, instead of re-executing the workflow from the top against the full event log.
246
+ - Short-lived runs below the threshold never pay the snapshot cost; long-running or unbounded runs stop scaling their resume cost with total event-log length. `1` snapshots at every qualifying suspension.
247
+ - Choosing a value: a restore costs roughly the same regardless of log length (tens of milliseconds to decompress and restore the heap, plus fetching the snapshot), while a full replay grows with the log (tens of milliseconds at a few hundred events, seconds at several thousand). Restoring starts to pay off at a few hundred events, so values in the hundreds suit most long-running workflows. Very low values mostly add snapshot saves to runs that replay quickly anyway.
248
+ - Snapshots are an optimization, not a source of truth: the event log remains authoritative, and a missing, corrupt, oversized, or incompatible snapshot automatically falls back to a full replay. Snapshots over 32 MB uncompressed are not saved.
249
+ - Like `WORKFLOW_VM`, the policy is stamped into the run's `executionContext` at start, so a run keeps the snapshot policy it started with. `start()` rejects an invalid value. An invalid value on the workflow handler disables snapshotting with a warning.
250
+ - A snapshot can only be restored by the exact QuickJS engine build that captured it. On Vercel, runs keep executing on the deployment they started on, so deploys don't affect in-flight snapshots. If your deployment serves in-flight runs from new code (for example, a self-hosted rolling update), an upgrade that changes the embedded engine makes every in-flight run fall back to a full replay at its next resume, all at once, before re-snapshotting at its next qualifying suspension. Plan for that one-time replay load when upgrading.
251
+
252
+ What a snapshot contains, and how it is protected:
253
+
254
+ - A snapshot is the workflow VM's memory: the workflow's code and in-memory state, including step results and hook payloads it holds, and the copy of `process.env` the VM exposes. It is executable state, so write access to snapshot storage is equivalent to running code in the workflow VM.
255
+ - Snapshots are compressed and then encrypted with the run's encryption key. The encryption also authenticates the snapshot: on restore, the runtime rejects any snapshot that isn't encrypted with the run's key, or whose stored metadata doesn't match the metadata sealed inside it.
256
+ - By default, runs without an encryption key are never snapshotted. Only world-vercel provides run encryption keys. On world-local and world-postgres, set `WORKFLOW_SNAPSHOT_ALLOW_UNENCRYPTED=1` on the workflow handler to snapshot anyway. Those snapshots are stored in plaintext (in `.workflow-data/snapshots/` or the `workflow_snapshots` table) and are not authenticated, so only enable this where snapshot storage is as trusted as your deployment.
257
+ - A restored run sees `process.env` as of the current invocation, the same as a full replay.
258
+ - Snapshots are deleted after the run reaches a terminal state. world-local and world-postgres have no separate retention for them, so a snapshot of a run that ends without any invocation observing it (for example, one cancelled externally) stays until you remove it.
259
+
260
+ ### `WORKFLOW_SNAPSHOT_ALLOW_UNENCRYPTED`
261
+
262
+ - Default: disabled
263
+ - Set `1` or `true` on the workflow handler to let the QuickJS engine persist VM snapshots for runs without an encryption key. See [`WORKFLOW_SNAPSHOT_THRESHOLD`](#workflow_snapshot_threshold) for what that stores.
264
+
240
265
  ## Dynamic workflows
241
266
 
242
267
  ### `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS`
@@ -259,6 +284,7 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
259
284
  - Default: automatic
260
285
  - Forces the write-side codec to `zstd` or `gzip`.
261
286
  - Invalid values are ignored. Automatic mode prefers `zstd` when the current Node.js runtime supports it, otherwise it falls back to `gzip`.
287
+ - Writes for a run on another deployment (for example `resumeHook()` reaching a run that started before the project moved to a newer Node.js version) use `zstd` only when that run's deployment runs Node.js 22.15+ (or 23.8+), and `gzip` otherwise, since older Node.js versions cannot decode `zstd`. Runs created before `@workflow/core` recorded the Node.js version always receive `gzip`. Setting this to `zstd` does not override that check.
262
288
 
263
289
  ### `WORKFLOW_TRACE_MODE`
264
290
 
@@ -290,7 +316,7 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
290
316
 
291
317
  Node's own modules do less than the client they replace, so enabling this drops the per-call-site tuning the Worlds configure:
292
318
 
293
- - Event-log requests lose HTTP/2, so concurrent reads and writes no longer share one connection, and the enlarged HTTP/2 receive windows no longer apply. This is the largest difference, and it slows down replays that read a big event log. It does not apply to event writes on the opt-in [WebSocket events transport](/docs/configuration/worlds#workflow_events_transport), which takes neither transport.
319
+ - Event-log requests lose HTTP/2, so concurrent reads and writes no longer share one connection, and the enlarged HTTP/2 receive windows no longer apply. This is the largest difference, and it slows down replays that read a big event log. It does not apply to event writes on the [WebSocket events transport](/docs/configuration/worlds#workflow_events_transport), which is the default and takes neither transport.
294
320
  - Requests lose their transport-level retry. Failures still surface to the layers above, which retry event writes and redeliver queue messages, so nothing is silently dropped, but a failure that a same-connection retry would have hidden now costs a full redelivery.
295
321
  - Stream close loses its retry of retriable server errors. A transient failure at close can leave a stream marked closing until the run expires, where it would previously have resolved on the retry.
296
322