workflow 5.0.1 → 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 (40) hide show
  1. package/docs/ai/chat-session-modeling.mdx +8 -8
  2. package/docs/ai/human-in-the-loop.mdx +2 -2
  3. package/docs/ai/index.mdx +14 -14
  4. package/docs/ai/streaming-updates-from-tools.mdx +2 -2
  5. package/docs/api-reference/workflow-nest/index.mdx +4 -1
  6. package/docs/api-reference/workflow-nest/is-workflow-request.mdx +58 -0
  7. package/docs/api-reference/workflow-nest/meta.json +1 -0
  8. package/docs/api-reference/workflow-nest/workflow-controller.mdx +6 -2
  9. package/docs/api-reference/workflow-nest/workflow-module.mdx +9 -1
  10. package/docs/configuration/framework-options.mdx +6 -0
  11. package/docs/configuration/runtime-tuning.mdx +2 -1
  12. package/docs/configuration/worlds.mdx +4 -4
  13. package/docs/cookbook/integrations/ai-sdk.mdx +8 -8
  14. package/docs/cookbook/integrations/chat-sdk.mdx +8 -8
  15. package/docs/cookbook/integrations/sandbox.mdx +8 -8
  16. package/docs/errors/node-js-module-in-workflow.mdx +36 -0
  17. package/docs/foundations/serialization.mdx +1 -1
  18. package/docs/foundations/streaming.mdx +1 -1
  19. package/docs/getting-started/astro.mdx +7 -9
  20. package/docs/getting-started/express.mdx +4 -10
  21. package/docs/getting-started/fastify.mdx +4 -9
  22. package/docs/getting-started/hono.mdx +4 -10
  23. package/docs/getting-started/index.mdx +2 -2
  24. package/docs/getting-started/nestjs.mdx +157 -36
  25. package/docs/getting-started/next.mdx +15 -18
  26. package/docs/getting-started/nitro.mdx +4 -10
  27. package/docs/getting-started/nuxt.mdx +4 -10
  28. package/docs/getting-started/react-router/v7.mdx +6 -22
  29. package/docs/getting-started/react-router/v8.mdx +6 -22
  30. package/docs/getting-started/sveltekit.mdx +7 -9
  31. package/docs/getting-started/tanstack-start.mdx +7 -9
  32. package/docs/getting-started/vite.mdx +7 -9
  33. package/docs/how-it-works/code-transform.mdx +12 -8
  34. package/docs/how-it-works/encryption.mdx +3 -1
  35. package/docs/whats-new.mdx +1 -1
  36. package/docs/worlds/building-a-world.mdx +6 -1
  37. package/docs/worlds/postgres.mdx +12 -32
  38. package/docs/worlds/upgrading-to-v5.mdx +1 -1
  39. package/docs/worlds/vercel.mdx +7 -5
  40. package/package.json +12 -12
@@ -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
 
@@ -189,7 +189,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr
189
189
  if (output) {
190
190
  return (
191
191
  <div className="border rounded-lg p-4">
192
- <p className="text-sm text-muted-foreground">{output}</p>
192
+ <p className="text-sm text-gray-900">{output}</p>
193
193
  </div>
194
194
  );
195
195
  }
@@ -211,7 +211,7 @@ export function BookingApproval({ toolCallId, input, output }: BookingApprovalPr
211
211
  <div className="border rounded-lg p-4 space-y-4">
212
212
  <div className="space-y-2">
213
213
  <p className="font-medium">Approve this booking?</p>
214
- <div className="text-sm text-muted-foreground">
214
+ <div className="text-sm text-gray-900">
215
215
  {input && (
216
216
  <div className="space-y-2">
217
217
  <div>Flight: {input.flightNumber}</div>
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]
@@ -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>
@@ -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`
@@ -284,6 +284,7 @@ What a snapshot contains, and how it is protected:
284
284
  - Default: automatic
285
285
  - Forces the write-side codec to `zstd` or `gzip`.
286
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.
287
288
 
288
289
  ### `WORKFLOW_TRACE_MODE`
289
290
 
@@ -315,7 +316,7 @@ What a snapshot contains, and how it is protected:
315
316
 
316
317
  Node's own modules do less than the client they replace, so enabling this drops the per-call-site tuning the Worlds configure:
317
318
 
318
- - 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.
319
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.
320
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.
321
322
 
@@ -317,8 +317,8 @@ When enabled (the default), a suspension's eager `step_created` and `wait_create
317
317
 
318
318
  - Factory option: none
319
319
  - CLI flag: none
320
- - Default: `http`
321
- - 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.
322
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
323
 
324
324
  ### `WORKFLOW_EVENTS_TRANSPORT_WS_OVERRIDE_WORKFLOWS`
@@ -326,8 +326,8 @@ When enabled (the default), a suspension's eager `step_created` and `wait_create
326
326
  - Factory option: none
327
327
  - CLI flag: none
328
328
  - Default: none
329
- - Comma-separated workflows whose runs use the WebSocket events transport even when `WORKFLOW_EVENTS_TRANSPORT` is `http` or unset. 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 when `WORKFLOW_EVENTS_TRANSPORT=ws`, which already enables every workflow.
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
331
  - See [`WORKFLOW_EVENTS_TRANSPORT_WS_OVERRIDE_WORKFLOWS`](/worlds/vercel#workflow_events_transport_ws_override_workflows) for details.
332
332
 
333
333
  ### `WORKFLOW_WS_MAX_MESSAGE_BYTES`
@@ -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.
@@ -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.
@@ -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