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.
- package/docs/advanced/dynamic-workflows.mdx +4 -1
- package/docs/advanced/index.mdx +13 -0
- package/docs/advanced/meta.json +5 -0
- package/docs/ai/chat-session-modeling.mdx +8 -8
- package/docs/ai/human-in-the-loop.mdx +7 -3
- package/docs/ai/index.mdx +14 -14
- package/docs/ai/streaming-updates-from-tools.mdx +2 -2
- package/docs/api-reference/vitest/index.mdx +2 -2
- package/docs/api-reference/workflow/create-hook.mdx +4 -0
- package/docs/api-reference/workflow/create-webhook.mdx +1 -1
- package/docs/api-reference/workflow/define-hook.mdx +4 -0
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +9 -3
- package/docs/api-reference/workflow-api/resume-hook.mdx +4 -0
- package/docs/api-reference/workflow-api/resume-webhook.mdx +4 -0
- package/docs/api-reference/workflow-globals.mdx +4 -0
- package/docs/api-reference/workflow-nest/index.mdx +4 -1
- package/docs/api-reference/workflow-nest/is-workflow-request.mdx +58 -0
- package/docs/api-reference/workflow-nest/meta.json +1 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +6 -2
- package/docs/api-reference/workflow-nest/workflow-module.mdx +9 -1
- package/docs/api-reference/workflow-runtime/world/storage.mdx +29 -0
- package/docs/configuration/build-and-diagnostics.mdx +11 -1
- package/docs/configuration/framework-options.mdx +6 -0
- package/docs/configuration/runtime-tuning.mdx +28 -2
- package/docs/configuration/worlds.mdx +28 -2
- package/docs/cookbook/common-patterns/webhooks.mdx +2 -1
- package/docs/cookbook/integrations/ai-sdk.mdx +8 -8
- package/docs/cookbook/integrations/chat-sdk.mdx +8 -8
- package/docs/cookbook/integrations/sandbox.mdx +8 -8
- package/docs/errors/node-js-module-in-workflow.mdx +36 -0
- package/docs/foundations/errors-and-retries.mdx +3 -3
- package/docs/foundations/hooks.mdx +91 -3
- package/docs/foundations/serialization.mdx +1 -1
- package/docs/foundations/streaming.mdx +1 -1
- package/docs/foundations/workflows-and-steps.mdx +1 -1
- package/docs/getting-started/astro.mdx +7 -9
- package/docs/getting-started/express.mdx +4 -10
- package/docs/getting-started/fastify.mdx +4 -9
- package/docs/getting-started/hono.mdx +4 -10
- package/docs/getting-started/index.mdx +2 -2
- package/docs/getting-started/nestjs.mdx +157 -36
- package/docs/getting-started/next.mdx +15 -18
- package/docs/getting-started/nitro.mdx +4 -10
- package/docs/getting-started/nuxt.mdx +4 -10
- package/docs/getting-started/react-router/v7.mdx +6 -22
- package/docs/getting-started/react-router/v8.mdx +6 -22
- package/docs/getting-started/sveltekit.mdx +7 -9
- package/docs/getting-started/tanstack-start.mdx +7 -9
- package/docs/getting-started/vite.mdx +7 -9
- package/docs/how-it-works/code-transform.mdx +13 -9
- package/docs/how-it-works/encryption.mdx +3 -1
- package/docs/meta.json +1 -1
- package/docs/testing/index.mdx +3 -5
- package/docs/whats-new.mdx +8 -2
- package/docs/worlds/building-a-world.mdx +68 -1
- package/docs/worlds/postgres.mdx +16 -32
- package/docs/worlds/upgrading-to-v5.mdx +29 -8
- package/docs/worlds/vercel.mdx +50 -4
- package/package.json +12 -12
package/docs/testing/index.mdx
CHANGED
|
@@ -91,13 +91,11 @@ For workflows that rely on runtime features like [hooks](/docs/foundations/hooks
|
|
|
91
91
|
### Installation
|
|
92
92
|
|
|
93
93
|
```package-install
|
|
94
|
-
npm i -D @workflow/vitest
|
|
94
|
+
npm i -D @workflow/vitest
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
<Callout type="warn">
|
|
98
|
-
**
|
|
99
|
-
|
|
100
|
-
The plugin builds and runs your workflows against the copy of `@workflow/core` it was installed with, so it checks this for you. At the start of each run it compares that copy with the one your app resolves: 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.
|
|
98
|
+
**Keep `@workflow/vitest` on the same major as `workflow`.** The plugin builds and runs your workflows against the copy of `@workflow/core` it was installed with, so it checks this for you. At the start of each run it compares that copy with the one your app resolves: 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.
|
|
101
99
|
</Callout>
|
|
102
100
|
|
|
103
101
|
### Vitest configuration
|
|
@@ -483,7 +481,7 @@ Integration tests are the right place to verify that your workflows handle error
|
|
|
483
481
|
|
|
484
482
|
### Upgrade `@workflow/vitest` with the SDK
|
|
485
483
|
|
|
486
|
-
`@workflow/vitest` carries its own copy of the Workflow runtime, so treat it as part of the same upgrade as `workflow
|
|
484
|
+
`@workflow/vitest` carries its own copy of the Workflow runtime, so treat it as part of the same upgrade as `workflow`: bump both together, and keep them on the same major. See [Installation](#installation).
|
|
487
485
|
|
|
488
486
|
## Further reading
|
|
489
487
|
|
package/docs/whats-new.mdx
CHANGED
|
@@ -20,7 +20,7 @@ npx skills add https://github.com/vercel/workflow --skill migrating-workflow-v4-
|
|
|
20
20
|
|
|
21
21
|
<Callout type="info">
|
|
22
22
|
Workflow SDK v4 remains installable as `workflow@4` and receives stability
|
|
23
|
-
fixes.
|
|
23
|
+
fixes. Its documentation lives at [/v4/docs](/v4/docs).
|
|
24
24
|
</Callout>
|
|
25
25
|
|
|
26
26
|
## Highlights
|
|
@@ -39,6 +39,10 @@ The largest change in v5 has no API surface: the runtime does far less work per
|
|
|
39
39
|
|
|
40
40
|
**Resuming a hook takes one round trip instead of two.** `resumeHook()` writes the `hook_received` event and dispatches the queue message concurrently, with a `(runId, resumeId)` dedup constraint keeping the two writers converging on exactly one event. See [Resilient hook resumption](/docs/changelog/resilient-resume).
|
|
41
41
|
|
|
42
|
+
**A suspension's writes go out as one batch.** The `step_created` and `wait_created` events a suspension produces are folded into a single durable write with a per-event outcome, instead of one request each, and a fan-out's inline step bodies start straight off that commit rather than each claiming its step first. This engages on Worlds that implement the batch API and can be turned off with [`WORKFLOW_BATCH_TRANSITIONS=0`](/docs/configuration/worlds#workflow_batch_transitions). See [Batched event writes](/docs/changelog/batched-event-writes).
|
|
43
|
+
|
|
44
|
+
**Concurrent writers no longer compete for a position in the log.** Each event's position used to be claimed by the write that filled it, so a wide fan-out serialized on that claim. Positions are now handed out ahead of the commit, which is what makes the fan-out above cheap. The cost is a position whose writer dies, which the backend closes with a `noop` event that replay steps over. Nothing about this is visible from workflow code; [`WORKFLOW_SEALED_LOG=0`](/docs/configuration/runtime-tuning#workflow_sealed_log) is the kill switch, and existing runs keep the scheme they were created on.
|
|
45
|
+
|
|
42
46
|
**Payloads are compressed** before they are encrypted and sent to the API. Repetitive payloads compress heavily; AI token streams average around 80% smaller. That is less stored data and less to move over the network.
|
|
43
47
|
|
|
44
48
|
On [Vercel Workflows](/worlds/vercel) these benefits compound to reduce compute costs by up to 80% for workflows made of many small steps, and storage cost by up to 70%, depending on the workload. Depending on your setup, you may see similar gains using self-hosted or third-party Worlds.
|
|
@@ -149,7 +153,9 @@ All three first-party Worlds now implement it: Vercel accepts up to 30 days, and
|
|
|
149
153
|
- **A misrouted delivery no longer fails a run.** Runs are pinned to the deployment that created them. A delivery that arrives at a different deployment is now re-routed to the pinned one with backoff instead of failing, and only gives up with the new [`DEPLOYMENT_MISMATCH`](/docs/errors/deployment-mismatch) error once the recovery budget is spent. Nothing executes on the wrong deployment while this happens. In 4.x the same situation surfaced as an unexplained decryption failure.
|
|
150
154
|
- **A run cannot be forked across environments.** `start()` stamps the environment it was called from onto the queue message, and a deployment refuses a delivery whose run was created in a different environment. Previously a preview client and a production deployment could each hold half of one run ID.
|
|
151
155
|
- **An experimental QuickJS VM engine.** Set [`WORKFLOW_VM=quickjs`](/docs/configuration/runtime-tuning#workflow_vm) to run workflow functions in a QuickJS VM compiled to WebAssembly instead of `node:vm`, for platforms that do not provide `node:vm`. Replay semantics are identical, but the available globals are not: check the differences before switching an existing deployment.
|
|
152
|
-
- **
|
|
156
|
+
- **Experimental dynamic workflows.** Pass workflow source as a string to `start()` to run orchestration assembled after deployment over steps already deployed with your app. The code is stored with the run (encrypted on Vercel), so replays always execute the code the run started with. Off unless the deployment sets `WORKFLOW_EXPERIMENTAL_DYNAMIC_WORKFLOWS=1`. See [Dynamic Workflows](/docs/advanced/dynamic-workflows).
|
|
157
|
+
- **A WebSocket transport for event writes** on the Vercel World, which ships a run's events over one socket instead of one HTTP request each. It is the default; set [`WORKFLOW_EVENTS_TRANSPORT=http`](/worlds/vercel#workflow_events_transport) to opt out. Tracing is unaffected: each write still emits an `http POST` client span, synthesized around the frame, with the transport on `workflow.events.transport`.
|
|
158
|
+
- **`await run.returnValue` waits instead of polling.** Reading a run's result long-polls the World until the run reaches a terminal status, rather than asking again on a fixed interval. A result is observed as soon as it exists, and an idle wait costs one open request instead of a request per tick. Worlds that do not implement the long poll keep the interval.
|
|
153
159
|
- **An event arriving mid-replay no longer fails the run.** A hook resume or step completion landing while a replay is in flight used to be able to fail it with [`CORRUPTED_EVENT_LOG`](/docs/errors/corrupted-event-log). Writes now come back with the events the replay had not seen, and the event is held for whichever part of the workflow awaits it. A run only fails when the log is genuinely missing a position.
|
|
154
160
|
|
|
155
161
|
## Breaking changes
|
|
@@ -130,7 +130,12 @@ interface Storage {
|
|
|
130
130
|
|
|
131
131
|
// Create an event for an existing run
|
|
132
132
|
create(runId: string, data: CreateEventRequest, params?: CreateEventParams): Promise<EventResult>;
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
// Optional: append an ordered list of events in one durable write, with a
|
|
135
|
+
// per-event outcome for each. Implementing it is the declaration; there is
|
|
136
|
+
// no capability flag.
|
|
137
|
+
createBatch?(runId: string, events: BatchEventRequest[], params?: CreateEventBatchParams): Promise<EventBatchResult>;
|
|
138
|
+
|
|
134
139
|
list(params: ListEventsParams): Promise<PaginatedResponse<Event>>;
|
|
135
140
|
listByCorrelationId(params: ListEventsByCorrelationIdParams): Promise<PaginatedResponse<Event>>;
|
|
136
141
|
};
|
|
@@ -151,6 +156,10 @@ interface Storage {
|
|
|
151
156
|
2. Atomically update the affected entity (run, step, or hook)
|
|
152
157
|
3. Return both the created event and the updated entity
|
|
153
158
|
|
|
159
|
+
**Batch writes:** `events.createBatch()` is optional, and implementing it is what declares it. The runtime folds a suspension's `step_created` and `wait_created` writes into batches only when the method exists, and otherwise takes the single-event path unchanged. Implement it with real atomicity per attempt, so a lost race leaves nothing behind, or do not implement it at all. The events land in request order at consecutive positions, and a concurrent writer may push the whole batch above the caller's view of the log. No skipped-event report accompanies the result, so a position-tracking caller compares the committed positions against what it expected and reloads. Reject the whole batch, with a request-level error, for `run_created`, `run_started`, `run_cancelled`, `hook_created`, `hook_disposed`, `attr_set`, for more events targeting one entity than a single write can express, and for a batch over your own size caps. The one legal same-entity pair is `step_created` followed by `step_started`, which creates the step born-running, and there the input must ride the `step_created`.
|
|
160
|
+
|
|
161
|
+
**Throttled step results:** `step_completed`, `step_retrying`, and a `step_failed` marked with `afterStepBody: true` in `CreateEventParams` record a step body that already ran. If your World retries throttled writes, keep these waiting longer than other writes, because losing one to queue redelivery runs the body again. `@workflow/world-vercel` waits until the invocation's deadline. The flag is advisory, and a World may ignore it.
|
|
162
|
+
|
|
154
163
|
**Run creation:** For `run_created` events, the `runId` parameter may be a client-provided string or `null`. When `null`, your World generates and returns a new `runId`.
|
|
155
164
|
|
|
156
165
|
**Event data resolution:** `events.list()` and `events.create()` accept `resolveData: 'skip-step-inputs'`, and the runtime replays with it. The World may leave `input` out of `step_created` and `step_started` events, and returns everything else as for `'all'`. Treat any value other than `'none'` as `'all'`, because a World that tests `resolveData === 'all'` strips step results and breaks every replay. Map the value with `entityResolveData()` from `@workflow/world` before passing it to an entity read.
|
|
@@ -163,6 +172,26 @@ Keep the owning run available for at least as long as its token remains unavaila
|
|
|
163
172
|
|
|
164
173
|
**Automatic hook cleanup:** When a run ends, remove its live hooks. Make each token available unless its `tokenRetentionUntil` is still in the future. A `hook_disposed` event always makes the token available immediately.
|
|
165
174
|
|
|
175
|
+
**Terminal-run start fence:** reject a `step_started` write once the run has
|
|
176
|
+
reached a terminal status, including a redelivery whose step row still reads
|
|
177
|
+
`running` because an earlier delivery claimed it. Starting work on a finished run
|
|
178
|
+
is never valid, and the outcome of such a step cannot be consumed by anything.
|
|
179
|
+
The queued-step path depends on this rejection, because a step message carries
|
|
180
|
+
run identity in place of a run fetch and has no other liveness check. A step that
|
|
181
|
+
was already in flight when the run ended still writes its `step_completed` or
|
|
182
|
+
`step_failed` unchanged: the fence is on starting, not on finishing.
|
|
183
|
+
|
|
184
|
+
**Listing runs:** `runs.list()` takes an optional `workflowName` and an optional
|
|
185
|
+
`status`, and `status` is either one `WorkflowRunStatus` or an array of them. An
|
|
186
|
+
array matches runs in *any* of the listed statuses, and an empty array matches
|
|
187
|
+
nothing, mirroring SQL `IN ()`. An unset filter means "every status", so treat
|
|
188
|
+
`status: []` and an omitted `status` as different requests. If your backend
|
|
189
|
+
cannot express the array form, reject it with an explicit error rather than
|
|
190
|
+
filtering on the first element or ignoring the filter: a silently wrong result
|
|
191
|
+
set is worse than a failed call. `@workflow/world` exports
|
|
192
|
+
`TERMINAL_WORKFLOW_RUN_STATUSES` so neither you nor your callers restate the
|
|
193
|
+
status vocabulary.
|
|
194
|
+
|
|
166
195
|
### Optional: waiting for a terminal run status
|
|
167
196
|
|
|
168
197
|
`await run.returnValue` must determine when a run finishes. Without help, it rereads the run every second, so it reports a run that finishes just after a read up to 1s late. Implement `runs.waitForTerminalStatus(id, { timeoutMs, signal, resolveData })` so the runtime asks once and receives an answer when the run ends.
|
|
@@ -324,11 +353,22 @@ interface WorkflowInvokePayload {
|
|
|
324
353
|
runId: string;
|
|
325
354
|
stepId?: string;
|
|
326
355
|
stepName?: string;
|
|
356
|
+
runContext?: RunDispatchContext; // Immutable run identity, step messages only
|
|
327
357
|
traceCarrier?: Record<string, string>; // OpenTelemetry context
|
|
328
358
|
requestedAt?: Date;
|
|
329
359
|
}
|
|
330
360
|
```
|
|
331
361
|
|
|
362
|
+
**Run identity on step messages:** a step-execution message carries `runContext`
|
|
363
|
+
with the run's `deploymentId`, `specVersion`, `startedAt` and `rootRunId`. Each
|
|
364
|
+
of those is fixed for the life of a run, so a consumer that receives them starts
|
|
365
|
+
the step from the message alone instead of fetching the run first. Run *status*
|
|
366
|
+
is deliberately absent: the liveness check for a queued step is the
|
|
367
|
+
`step_started` write itself, which your World must reject on a terminal run (see
|
|
368
|
+
[Key implementation details](#key-implementation-details)). Messages from older
|
|
369
|
+
producers omit `runContext` and take the older path that fetches the run, so a
|
|
370
|
+
World needs to do nothing to support either.
|
|
371
|
+
|
|
332
372
|
The SDK also sends an internal `HealthCheckPayload` through the same workflow queue.
|
|
333
373
|
|
|
334
374
|
### Implementation considerations
|
|
@@ -350,6 +390,13 @@ interface Streamer {
|
|
|
350
390
|
streamFlushIntervalMs?: number;
|
|
351
391
|
|
|
352
392
|
streams: {
|
|
393
|
+
// Optional. A stateful writer lifetime; see below.
|
|
394
|
+
createWriteSession?(
|
|
395
|
+
runId: string,
|
|
396
|
+
name: string,
|
|
397
|
+
options: { writerId: `wrtr_${string}` }
|
|
398
|
+
): StreamWriteSession;
|
|
399
|
+
|
|
353
400
|
write(
|
|
354
401
|
runId: string,
|
|
355
402
|
name: string,
|
|
@@ -396,6 +443,21 @@ interface Streamer {
|
|
|
396
443
|
Streams are identified by a combination of `runId` and `name`. Each workflow run can have multiple named streams.
|
|
397
444
|
`writeMulti()` is an optional optimization for batching multiple writes.
|
|
398
445
|
|
|
446
|
+
`createWriteSession()` is optional. Implement it when your transport can do
|
|
447
|
+
better by holding state across a writer's chunks, for example by keeping one
|
|
448
|
+
connection open instead of reopening it per write. The runtime creates at most
|
|
449
|
+
one session per in-memory `WritableStream` and passes a `writerId` identifying
|
|
450
|
+
that lifetime, so the session's `write(chunkSeq, chunks)` receives a sequence
|
|
451
|
+
number that is writer-local rather than stream-global. That is what lets a
|
|
452
|
+
World order concurrent writers to the same stream. The session's `close()` must
|
|
453
|
+
not resolve before every prior write is durable; `dispose()` is optional and
|
|
454
|
+
releases transport resources without ending the stream. Optional `release()`
|
|
455
|
+
retires an idle transport after the runtime drains a released writer. Unlike
|
|
456
|
+
`dispose()`, it must leave later `write()` and `close()` usable, for example
|
|
457
|
+
by switching that writer to stateless HTTP. A World that does not
|
|
458
|
+
implement `createWriteSession` is unaffected: the runtime uses
|
|
459
|
+
`write`/`writeMulti`/`close` exactly as before.
|
|
460
|
+
|
|
399
461
|
`getChunks` returns a paginated snapshot of currently available chunks, unlike `get`, which returns a live `ReadableStream` that waits for new chunks. `getInfo` returns the tail index (last chunk index, 0-based, or `-1` when empty) and whether the stream is complete. Use this information to resolve negative `startIndex` values into absolute positions.
|
|
400
462
|
|
|
401
463
|
## Analytics interface (optional)
|
|
@@ -422,7 +484,11 @@ interface Analytics {
|
|
|
422
484
|
};
|
|
423
485
|
events: {
|
|
424
486
|
get(runId: string, eventId: string): Promise<AnalyticsEvent>;
|
|
487
|
+
// Bounded batch lookup within one run. Not paginated; rows that analytics
|
|
488
|
+
// has not ingested yet are omitted rather than throwing.
|
|
489
|
+
getMany(runId: string, eventIds: readonly string[]): Promise<AnalyticsEvent[]>;
|
|
425
490
|
list(params: AnalyticsListEventsParams): Promise<PaginatedResponse<AnalyticsEvent>>;
|
|
491
|
+
/** @deprecated A special case of `list({ runId, correlationId })`. Removed in the next major. */
|
|
426
492
|
listByCorrelationId(params: AnalyticsListEventsByCorrelationIdParams): Promise<PaginatedResponse<AnalyticsEvent>>;
|
|
427
493
|
};
|
|
428
494
|
hooks: {
|
|
@@ -441,6 +507,7 @@ If you implement this namespace, observe the following requirements:
|
|
|
441
507
|
- **Metadata only.** Analytics responses must not include run inputs or outputs, step data, hook tokens, or other payload data. The `Analytics*` schemas exported by `@workflow/world` define the complete set of permitted fields. Payload retrieval remains exclusively available through the Storage APIs.
|
|
442
508
|
- **Attribute filters use the latest value.** `runs.list({ attributes })` evaluates each filter against the run's most recently written value for that key. A request may contain up to eight key-value pairs. Reserved `$`-prefixed attributes are valid filters, although users cannot write them directly.
|
|
443
509
|
- **Time boundaries must be paired.** `startTime` and `endTime` may either both be omitted or both be supplied. Responses may include `pageInfo` describing retention and the available query window. Implementations with retention limits should return this information so tooling can present valid date ranges.
|
|
510
|
+
- **Page limits are part of the contract.** `pagination.limit` defaults to 40 and caps at 1000 for the run-scoped listings (`steps.list`, `events.list`, `waits.list`) and at 100 for the cross-run ones (`runs.list`, `attributes.list`, `hooks.list`); `events.getMany` accepts 1 to 100 ids. Callers hit these bounds before a request leaves their process, as a `RangeError`, so a request that reaches your implementation is already within them.
|
|
444
511
|
- **Results may be eventually consistent.** Analytics records may lag live workflow state. Consumers use this namespace for discovery and listing; Storage remains the authoritative interface for current workflow state and payload access.
|
|
445
512
|
|
|
446
513
|
See the [Analytics API reference](/docs/api-reference/workflow-runtime/world/analytics) for per-method parameters, row shapes, and `pageInfo` semantics.
|
package/docs/worlds/postgres.mdx
CHANGED
|
@@ -48,42 +48,22 @@ WORKFLOW_POSTGRES_URL="postgres://user:password@host:5432/database"
|
|
|
48
48
|
|
|
49
49
|
Run the migration script to create the necessary tables in your database. Ensure `WORKFLOW_POSTGRES_URL` or `DATABASE_URL` is set when running this command:
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<Tab value="npm">
|
|
54
|
-
|
|
55
|
-
```bash
|
|
51
|
+
```bash tab="npm"
|
|
56
52
|
npx --package=@workflow/world-postgres bootstrap
|
|
57
53
|
```
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<Tab value="pnpm">
|
|
62
|
-
|
|
63
|
-
```bash
|
|
55
|
+
```bash tab="pnpm"
|
|
64
56
|
pnpm dlx --package @workflow/world-postgres bootstrap
|
|
65
57
|
```
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<Tab value="Yarn">
|
|
70
|
-
|
|
71
|
-
```bash
|
|
59
|
+
```bash tab="Yarn"
|
|
72
60
|
yarn dlx --package @workflow/world-postgres bootstrap
|
|
73
61
|
```
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<Tab value="Bun">
|
|
78
|
-
|
|
79
|
-
```bash
|
|
63
|
+
```bash tab="Bun"
|
|
80
64
|
bunx --package @workflow/world-postgres bootstrap
|
|
81
65
|
```
|
|
82
66
|
|
|
83
|
-
</Tab>
|
|
84
|
-
|
|
85
|
-
</Tabs>
|
|
86
|
-
|
|
87
67
|
<Callout type="info">
|
|
88
68
|
The migration is idempotent and can safely be run as a post-deployment lifecycle script.
|
|
89
69
|
</Callout>
|
|
@@ -154,9 +134,9 @@ still adds overhead. Pulling in `workflow/runtime` from a server-startup hook pu
|
|
|
154
134
|
the whole runtime into the cold-start path before the first request is served.
|
|
155
135
|
</Callout>
|
|
156
136
|
|
|
157
|
-
<
|
|
137
|
+
<TabsWithChildren tabs={["Next.js","SvelteKit","Nitro"]}>
|
|
158
138
|
|
|
159
|
-
<
|
|
139
|
+
<TabContent order={1}>
|
|
160
140
|
|
|
161
141
|
Create an `instrumentation.ts` file in your project root:
|
|
162
142
|
|
|
@@ -174,9 +154,9 @@ export async function register() {
|
|
|
174
154
|
Learn more about [Next.js Instrumentation](https://nextjs.org/docs/app/guides/instrumentation).
|
|
175
155
|
</Callout>
|
|
176
156
|
|
|
177
|
-
</
|
|
157
|
+
</TabContent>
|
|
178
158
|
|
|
179
|
-
<
|
|
159
|
+
<TabContent order={2}>
|
|
180
160
|
|
|
181
161
|
Create a `src/hooks.server.ts` file:
|
|
182
162
|
|
|
@@ -194,9 +174,9 @@ export const init: ServerInit = async () => {
|
|
|
194
174
|
Learn more about [SvelteKit Hooks](https://svelte.dev/docs/kit/hooks).
|
|
195
175
|
</Callout>
|
|
196
176
|
|
|
197
|
-
</
|
|
177
|
+
</TabContent>
|
|
198
178
|
|
|
199
|
-
<
|
|
179
|
+
<TabContent order={3}>
|
|
200
180
|
|
|
201
181
|
Create a plugin to start the world on server initialization:
|
|
202
182
|
|
|
@@ -225,9 +205,9 @@ export default defineNitroConfig({
|
|
|
225
205
|
Learn more about [Nitro Plugins](https://v3.nitro.build/docs/plugins).
|
|
226
206
|
</Callout>
|
|
227
207
|
|
|
228
|
-
</
|
|
208
|
+
</TabContent>
|
|
229
209
|
|
|
230
|
-
</
|
|
210
|
+
</TabsWithChildren>
|
|
231
211
|
|
|
232
212
|
<Callout type="info">
|
|
233
213
|
The Postgres World requires a long-lived worker process that polls the database for jobs. This does not work on serverless environments. For Vercel deployments, use the [Vercel World](/worlds/vercel) instead.
|
|
@@ -278,6 +258,10 @@ Number of concurrent workers polling for jobs. Default: `50`.
|
|
|
278
258
|
|
|
279
259
|
This value also bounds how many parent→child workflow polls can be in flight simultaneously. Every `await childRun.returnValue` inside a workflow holds a worker slot until the child run terminates. If you expect recursive or highly-fanned-out parent/child workflows, raise this ceiling above the peak number of concurrent polls. With the default of 50, the included `fibonacciWorkflow` end-to-end test (`fib(6)`, about 24 concurrent polls at peak) passes; deeper recursion or larger fanouts need a correspondingly larger setting.
|
|
280
260
|
|
|
261
|
+
### `WORKFLOW_POSTGRES_POLL_INTERVAL_MS`
|
|
262
|
+
|
|
263
|
+
Milliseconds between idle job fetches per worker. Each worker polls on its own, so an idle process runs about `queueConcurrency × 1000 / pollInterval` fetches per second. Default: `500`.
|
|
264
|
+
|
|
281
265
|
### `WORKFLOW_POSTGRES_MAX_POOL_SIZE`
|
|
282
266
|
|
|
283
267
|
Maximum size of the internal `pg.Pool` used when `createWorld()` constructs the pool. Default: the `pg` default (`10`).
|
|
@@ -36,27 +36,36 @@ We're working on bringing back World compatibility tests and reporting on the [W
|
|
|
36
36
|
|
|
37
37
|
## Spec versions
|
|
38
38
|
|
|
39
|
-
A World declares the protocol version it speaks on `specVersion`, and that number is stamped on every run it creates. Declare `
|
|
39
|
+
A World declares the protocol version it speaks on `specVersion`, and that number is stamped on every run it creates. Declare `mintedSpecVersion()` from `@workflow/world`, not a literal:
|
|
40
40
|
|
|
41
41
|
{/* @skip-typecheck - partial World, the other members are elided */}
|
|
42
42
|
```typescript
|
|
43
|
-
import {
|
|
43
|
+
import { mintedSpecVersion } from '@workflow/world';
|
|
44
44
|
|
|
45
45
|
export function createWorld(): World {
|
|
46
46
|
return {
|
|
47
|
-
specVersion:
|
|
47
|
+
specVersion: mintedSpecVersion(),
|
|
48
48
|
// ...
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
In v4 the runtime required that number to equal its own current version exactly. In v5 it checks the declaration against a range
|
|
53
|
+
In v4 the runtime required that number to equal its own current version exactly. In v5 it checks the declaration against a range before it creates or replays anything, and refuses a World outside it with an error naming both the range and what your World declared. The floor is the version that introduced [slot-numbered event IDs](#event-id-allocation), because a World below it allocates IDs the runtime cannot read positions out of, and admitting one would only move the failure from startup into the middle of a run. The ceiling is the highest version this runtime can read.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
`mintedSpecVersion()` is a function rather than a constant because the version a World stamps is a deployment-level choice. It answers with the sealed-log version by default, and with the slot-identity version when [`WORKFLOW_SEALED_LOG=0`](/docs/configuration/runtime-tuning#workflow_sealed_log) opts new runs out. Both sit inside the accepted range, so either answer is a valid declaration. Reading it per `createWorld()` call rather than once at module load is what lets a single process create Worlds in both modes.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Calling it is also what keeps the check passing across upgrades, since it moves with the `@workflow/world` version your package resolves. A hard-coded number leaves your World a version behind the next bump and gets it rejected by the runtime it ships alongside. That includes the constants: `SPEC_VERSION_CURRENT` and `SPEC_VERSION_SUPPORTS_SLOT_IDENTITY` are literals by another name for this purpose, since neither follows the sealed-log setting. Keep `@workflow/world` in the same release channel as the `workflow` version your users install.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
### Sealed logs and `noop` events
|
|
60
|
+
|
|
61
|
+
The sealed-log version exists for a World whose store makes allocating a position at the commit a contention bottleneck. Such a World may hand positions out from a per-run counter *before* the commit, so concurrent writers never race for one, and then restore density at read time by writing a `noop` event into any position it can prove was abandoned. A `noop` occupies its position and means nothing: replay steps over it without delivering it and without advancing the deterministic clock.
|
|
62
|
+
|
|
63
|
+
Two consequences for an implementation:
|
|
64
|
+
|
|
65
|
+
- **If you allocate at the commit, you are already compliant** and have nothing to build. No write can leave a position empty, so you have no holes to seal and will never emit a `noop`. `@workflow/world-local` and `@workflow/world-postgres` are in this position.
|
|
66
|
+
- **What the version actually gates is the reader.** A run stamped at the sealed-log version can only be replayed by a reader that knows to skip `noop`. That is every runtime on this release train, but a runtime pinning its own accepted range separately, such as the Python runtime, has to catch up first. `WORKFLOW_SEALED_LOG=0` is the switch for an environment where it has not.
|
|
67
|
+
|
|
68
|
+
Runs carry a spec version too, and a run keeps the version it was created under for its whole life. Read the stamped version off the run rather than assuming every run matches what your World declares today. Changing what you stamp does not reach runs already in your store: their version is persisted, every version test in the runtime is a lower bound, and a run's event ID scheme is resolved from what is stored.
|
|
60
69
|
|
|
61
70
|
## Interface changes
|
|
62
71
|
|
|
@@ -84,6 +93,14 @@ These do not change any signature, so an implementation ported by types alone wi
|
|
|
84
93
|
|
|
85
94
|
**A stale replay no longer has to be refused.** v5 shipped with a `preconditionGuard` capability for a World that rejected an event creation whose snapshot was behind the log. It is gone, and nothing replaced it: allocating positions at the commit means a reader's log is a prefix rather than a prefix with a hole, replay is deterministic on a prefix, and a write reports the events it was pushed past. As a result, a stale replay costs a merge instead of a rejection. If you implemented the guard, you can delete it. `PreconditionFailedError` and the runtime's handling of it remain for a World that allocates positions away from the commit (see [Event ID allocation](#event-id-allocation)); no World in the SDK throws it.
|
|
86
95
|
|
|
96
|
+
**Process-wide state has to live on `globalThis`.** A module's top-level `const` or `let` is one instance per *module instance*, not per process, and a host server routinely holds several. Next.js compiles its server output into independent module graphs, and a bundled module is compiled into each one with its own module-scope bindings. Since `@workflow/world-vercel` moved from external to bundled, every module-scope singleton in it quietly became one per layer. The visible casualty was the WebSocket events transport: the queue consumer registered its channel in the route copy's registry while the write path looked it up in the instrumentation copy's empty one, so every event silently fell back to HTTP for the life of the process.
|
|
97
|
+
|
|
98
|
+
This bites rather than merely wasting memory because the runtime caches the *World object* process-wide while any module state that World closes over stays layer-local. Anything your World reaches at request time therefore has to be process-wide too: connection pools, transport registries, ID factories, caches, and log-once latches. Hold them in one object behind [`globalSingleton()`](https://github.com/vercel/workflow/blob/main/packages/utils/src/global-singleton.ts) from `@workflow/utils`, which keys the object off a `Symbol.for` on `globalThis`. A `let` cannot be shared by reference, so a latch becomes a field on that object.
|
|
99
|
+
|
|
100
|
+
**One World per process.** The workflow entrypoint's queue handler is now built from the runtime World that `getWorld()` returns, rather than from `getWorldHandlers()`. A stateful World is no longer instantiated twice in one process, so it stops getting duplicate connection pools and duplicate queue workers. If you added your own de-duplication to work around that, it is now redundant, though harmless if it keys on process-wide state.
|
|
101
|
+
|
|
102
|
+
**Your own transport is your own business, except for the tracing.** How a World ships events to its backend is unconstrained: `@workflow/world-vercel` uses a WebSocket by default and falls back to HTTP. What is constrained is what a reader of a trace sees. A non-HTTP transport still has to emit the per-event client span that an HTTP write would, or the per-event view of a run silently disappears. See [`WORKFLOW_EVENTS_TRANSPORT`](/worlds/vercel#workflow_events_transport) for the span shape and attributes the Vercel World uses, including a separate span for the handshake.
|
|
103
|
+
|
|
87
104
|
**Replay reads the event log with `resolveData: 'skip-step-inputs'`.** A World may leave `input` out of `step_created` and `step_started` events for this value, and must otherwise treat it as `'all'`. A World that tests `resolveData === 'all'`, or validates against `['none', 'all']`, strips step results or rejects the read, and every replay fails. Test for `'none'` instead, or map with `entityResolveData()` from `@workflow/world`. `@workflow/world-testing` covers this case.
|
|
88
105
|
|
|
89
106
|
**Event creation can return a delta.** `events.create()` may return events alongside the one it created, in `events` with a matching `cursor` and `hasMore`. The runtime uses this to skip a follow-up `events.list` round trip on `run_started`, on step-terminal writes that carried a `sinceCursor`, and on `hook_received` writes that carried `preloadEvents`. All three are advisory: a World that returns only the created event stays correct and pays one more round trip.
|
|
@@ -103,6 +120,8 @@ The scheme exists for what a reader can conclude from a log it just fetched: pos
|
|
|
103
120
|
- **Bump and report.** `events.create()` params carry `eventCount`, so the expected position is `eventCount + 1`. When it is taken, do not reject the write: commit at the next free position and return the events you skipped on the success response. A stale count is the normal case for a parallel fan-out, and rejecting it would serialize writes the runtime deliberately issues concurrently.
|
|
104
121
|
- **Allocate at the commit.** Take the position in the same operation that appends the event, not earlier. This is what makes a reader's log a prefix of the run's log rather than a prefix with a hole in it: nothing can land behind a position a reader has already passed. A World that mints a position in a request handler and commits later breaks the property every replay depends on, and is the only kind that still has a use for a stale-write rejection.
|
|
105
122
|
|
|
123
|
+
The one sanctioned exception is the sealed log, which is what the [sealed-log spec version](#sealed-logs-and-noop-events) is for: a World may pre-assign positions if it also seals the holes that leaves. Everything below assumes you allocate at the commit, which is the simpler contract and the one both first-party non-Vercel Worlds keep.
|
|
124
|
+
|
|
106
125
|
[Event ID Allocation](/worlds/building-a-world#event-id-allocation) carries the full rules, and [Event IDs](/docs/how-it-works/event-sourcing#event-ids) covers what the format means for anything that reads an ID back.
|
|
107
126
|
|
|
108
127
|
One consequence is specific to an upgrade, and it is the thing to plan around.
|
|
@@ -118,6 +137,8 @@ None of this is required. Each entry is a hook the runtime uses if your World pr
|
|
|
118
137
|
| Member | What it buys |
|
|
119
138
|
| --- | --- |
|
|
120
139
|
| `capabilities` | Advertises `hookRetention.active`, `hookResumeDedup`, `hookForceClaim`, `deploymentAffinity`, `maxConcurrency`, and `dynamicWorkflowCode`. See the contract note above about failing closed. Event ID allocation is *not* in here: it is a requirement, not a capability. |
|
|
140
|
+
| `events.createBatch` | Appends an ordered list of events in one durable write, with a per-event outcome for each. Implementing the method *is* the declaration: the runtime folds a suspension's `step_created` / `wait_created` writes into batches only when it exists, and otherwise takes the single-event path unchanged. Implement it with real atomicity per attempt, so a lost race leaves nothing behind, or leave it out. See [Batched event writes](/docs/changelog/batched-event-writes). |
|
|
141
|
+
| `runs.waitForTerminalStatus` | Long-polls until a run reaches a terminal status. `await run.returnValue` uses it when present, instead of polling on an interval. |
|
|
121
142
|
| `analytics` | A metadata-only read namespace for observability surfaces. Payload-bearing reads stay on `runs`, `steps`, `events`, and `hooks`. |
|
|
122
143
|
| `runs.experimentalSetAttributes` | Backs `setAttributes()` from application code. Without it, run attributes are unavailable. |
|
|
123
144
|
| `runs.cancelMany` | Bulk cancellation: up to 500 unique run IDs per request (`BULK_CANCEL_MAX_RUN_IDS`), an optional `cancelReason` of at most 512 characters, and a per-run outcome for every ID. Without it, the runtime falls back to bounded-concurrency individual cancels. |
|
|
@@ -140,7 +161,7 @@ Compiling workflow files changed independently of the storage contract.
|
|
|
140
161
|
| Change | What to do |
|
|
141
162
|
| --- | --- |
|
|
142
163
|
| The `client` SWC transform mode was removed | It merged into `step` mode. Integrations passing `mode: 'client'` pass `mode: 'step'`. |
|
|
143
|
-
| `stepEntrypoint` removed from `workflow/runtime` | Steps execute through the combined workflow handler the framework integrations generate.
|
|
164
|
+
| `stepEntrypoint` removed from `workflow/runtime` | Steps execute through the combined workflow handler the framework integrations generate. A custom host builds that handler from the World `getWorld()` returns. `getWorldHandlers()` still exists for the build-time view of a World, which is what a build integration wants; it is no longer how a request-time handler is assembled. |
|
|
144
165
|
| Step, workflow and webhook bundles are ESM | Generated output moved from CJS to ESM, with a `createRequire` banner for CJS dependencies. The VM-executed workflow bundle stays CJS. The CLI's standalone output is renamed to match: `flow.mjs`, `webhook.mjs`, and `__step_registrations.mjs` in place of `flow.js`, `webhook.js`, and `step.js`. Consumers import the namespace rather than a default. |
|
|
145
166
|
| `workflow/internal/private` and `@workflow/core/private` removed | These were never public API. The compiler no longer emits imports from them, so regenerate build output rather than importing them yourself. |
|
|
146
167
|
| Duplicate step or workflow IDs fail the build | 4.x resolved collisions across non-exported workspace files last-write-wins. A build integration that derived IDs from a partial path may now produce build failures. |
|
package/docs/worlds/vercel.mdx
CHANGED
|
@@ -246,17 +246,39 @@ Experimental stream-write transport capability. Default: `http`. Set `WORKFLOW_S
|
|
|
246
246
|
|
|
247
247
|
This setting does not own tenant rollout policy and does not infer server support from package versions. HTTP remains the compatibility path. The protocol route (`/websockets/v1`) is versioned independently from REST v2/v4 and persisted workflow `specVersion` values.
|
|
248
248
|
|
|
249
|
-
Writes and close requests execute serially. The first operation waits up to 250 ms for an opted-in socket to open; this is an implementation-level measurement knob, not protocol semantics. If the budget expires, or the upgrade fails or is declined before acceptance, the writer uses HTTP without racing the same operation over both transports. After the socket accepts a write, a missing acknowledgement has an unknown outcome: the writer fails rather than replaying the write over HTTP and risking a duplicate append.
|
|
249
|
+
Writes and close requests execute serially. The first operation waits up to 250 ms for an opted-in socket to open; this is an implementation-level measurement knob, not protocol semantics. If the budget expires, or the upgrade fails or is declined before acceptance, the writer uses HTTP without racing the same operation over both transports. After the socket accepts a write, a missing acknowledgement has an unknown outcome: the writer fails rather than replaying the write over HTTP and risking a duplicate append. A throttled (429) write or close did not apply, so the writer retries it after the server's `Retry-After`, as the HTTP writer does, on the socket if it stays open (or its replacement, after a drain) and otherwise over HTTP. Past 30 seconds of cumulative wait, the write moves to HTTP, whose own 429 retry applies. A close that fails with a 5xx is retried over HTTP, since close is idempotent.
|
|
250
|
+
|
|
251
|
+
Once a released writer's writes drain, its socket closes without closing the shared stream, and later writes through that same handle go over HTTP. Writers acquired with `getWritable()` inside a step release their socket when the step completes. External `getRun(runId).getWritable()` handles release it on the first `releaseLock()` after pending writes drain, so hold the writer lock for the whole burst to keep writing over the socket.
|
|
250
252
|
|
|
251
253
|
Before routine authentication expiry or server max duration, the server sends a v1 `drain` control. The client stops sending, lets an already-admitted request receive its reply, and reconnects after the server closes with code 1001. Authentication drains re-resolve a fresh bearer. A request that was sent but receives no reply before close still has an unknown outcome and is never replayed.
|
|
252
254
|
|
|
255
|
+
Each stream WebSocket message is at most [`WORKFLOW_WS_MAX_MESSAGE_BYTES`](#workflow_ws_max_message_bytes) bytes. A write group larger than the limit is sent as several ordered write requests. A single chunk too large for one message is written over HTTP instead, as are the rest of its writer's writes.
|
|
256
|
+
|
|
253
257
|
### `WORKFLOW_EVENTS_TRANSPORT`
|
|
254
258
|
|
|
255
|
-
|
|
259
|
+
Workflow run events ship to the Vercel World over a WebSocket instead of one HTTP request each. Default: `ws`.
|
|
260
|
+
|
|
261
|
+
Set `WORKFLOW_EVENTS_TRANSPORT=http` to opt out. Only that exact value disables the WebSocket — any other value, including unset or empty, takes it — so a typo fails toward the default rather than silently pinning a deployment to HTTP.
|
|
256
262
|
|
|
257
|
-
|
|
263
|
+
The setting is ignored when the World is configured with `projectConfig` and therefore 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. The fallback is silent — now that the WebSocket is the default, nothing asked for it — and is reported once per process under `DEBUG=workflow:*`. `workflow.events.transport` on the per-write span records which transport actually carried a run.
|
|
264
|
+
|
|
265
|
+
Event writes use the WebSocket only while that run's events channel is open. The queue handler opens it for every delivery, so workflows need nothing extra. Code that writes a run's events outside a delivery, such as a custom driver or a long-lived process, opens the channel with `openEventsChannel(runId)`, or those writes go over HTTP. Call the returned release when you're done, because an open socket keeps the process alive:
|
|
266
|
+
|
|
267
|
+
{/*@skip-typecheck: incomplete code sample*/}
|
|
258
268
|
|
|
259
|
-
|
|
269
|
+
```typescript title="write-events.ts" lineNumbers
|
|
270
|
+
import { createWorld, openEventsChannel } from "@workflow/world-vercel";
|
|
271
|
+
|
|
272
|
+
const world = createWorld();
|
|
273
|
+
const release = openEventsChannel(runId);
|
|
274
|
+
try {
|
|
275
|
+
await world.events.create(runId, event);
|
|
276
|
+
} finally {
|
|
277
|
+
release?.();
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`openEventsChannel()` returns `undefined`, and writes stay on HTTP, when the transport is disabled or the World cannot hold a socket, such as a `projectConfig` World.
|
|
260
282
|
|
|
261
283
|
Tracing is unaffected by the choice. Each event write emits an `http POST` client span against the same `url.full`, regardless of which transport carries it. On the WebSocket path, the span is synthesized around the frame because no HTTP request is made. Attributes distinguish the transports:
|
|
262
284
|
|
|
@@ -267,9 +289,33 @@ Tracing is unaffected by the choice. Each event write emits an `http POST` clien
|
|
|
267
289
|
| `network.protocol.name` | — | `websocket` |
|
|
268
290
|
| `workflow.events.ws.url` | — | the socket the frame went over |
|
|
269
291
|
| `workflow.events.ws.req_id` | — | per-connection request id, matching the server's log line |
|
|
292
|
+
| `workflow.events.ws.request_parts` | — | messages the request was sent as, when it was split |
|
|
293
|
+
| `workflow.events.ws.reply_parts` | — | messages the reply arrived as, when it was split |
|
|
270
294
|
|
|
271
295
|
The WebSocket handshake is itself a span, `workflow.events.ws.connect`, so the cost of opening (or eagerly reopening) a connection is attributable rather than showing up as unexplained time inside the first write.
|
|
272
296
|
|
|
297
|
+
### `WORKFLOW_EVENTS_TRANSPORT_WS_OVERRIDE_WORKFLOWS`
|
|
298
|
+
|
|
299
|
+
Comma-separated workflows whose runs use the WebSocket events transport even when [`WORKFLOW_EVENTS_TRANSPORT`](#workflow_events_transport) is `http`. Use it to move selected workflows onto the WebSocket while the rest of the deployment stays on HTTP. Default: none.
|
|
300
|
+
|
|
301
|
+
Each entry is a workflow's function name, such as `processOrder`, or its full workflow name, such as `workflow//./src/workflows/order//processOrder`. Matching is exact and case-sensitive. A function name matches every workflow with that name, in any file.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
WORKFLOW_EVENTS_TRANSPORT_WS_OVERRIDE_WORKFLOWS=processOrder,syncInventory
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The override applies to the events channel the queue handler opens for each delivery of a listed workflow's run. The workflow name comes from the delivery's queue, so it covers the run's workflow and step invocations alike. `openEventsChannel(runId, config, { workflowName })` applies it to channels you open yourself; without `workflowName`, a deployment with `WORKFLOW_EVENTS_TRANSPORT=http` opens none. Unless `WORKFLOW_EVENTS_TRANSPORT=http` is set, every workflow already uses the WebSocket and the override has no effect.
|
|
308
|
+
|
|
309
|
+
Like other environment variables, the setting is fixed for each deployment, and a run stays on the deployment it started on. Changing it takes effect for runs started on the next deployment; runs already in progress keep the transport of the deployment they're pinned to.
|
|
310
|
+
|
|
311
|
+
### `WORKFLOW_WS_MAX_MESSAGE_BYTES`
|
|
312
|
+
|
|
313
|
+
Largest WebSocket message the Vercel World sends, header included, on both the events and stream-write transports. Default: `12582912` (12 MiB).
|
|
314
|
+
|
|
315
|
+
Some WebSocket paths limit the size of a single message, commonly to 16 MiB. Event payloads can be larger: a step's input or output is carried inside its event. A frame over this limit is sent as several messages, and the other side rebuilds it before handling it. Stream writes split a large write group into several ordered write requests instead (see [`WORKFLOW_STREAMS_TRANSPORT`](#workflow_streams_transport)). Frames at or under the limit are unaffected. Integer values are clamped to `2097152`-`16777216` (2–16 MiB) with a warning; other values use the default.
|
|
316
|
+
|
|
317
|
+
The client splits its own large requests, which needs a Vercel World backend that accepts split frames. It lists `frame-parts` in the `x-workflow-ws-flags` header of the WebSocket upgrade, and the backend splits large replies only for a client that does, so older clients keep receiving whole replies.
|
|
318
|
+
|
|
273
319
|
### Programmatic configuration
|
|
274
320
|
|
|
275
321
|
`createWorld()` accepts explicit API configuration. It does not read `WORKFLOW_VERCEL_*` automatically, so pass the environment values yourself when you want a configured World module:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -58,19 +58,19 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@workflow/astro": "5.0.
|
|
62
|
-
"@workflow/cli": "5.0.
|
|
63
|
-
"@workflow/core": "5.
|
|
64
|
-
"@workflow/errors": "5.0.
|
|
61
|
+
"@workflow/astro": "5.0.2",
|
|
62
|
+
"@workflow/cli": "5.0.2",
|
|
63
|
+
"@workflow/core": "5.1.0",
|
|
64
|
+
"@workflow/errors": "5.0.2",
|
|
65
65
|
"@workflow/typescript-plugin": "5.0.0",
|
|
66
|
-
"@workflow/utils": "5.0.
|
|
66
|
+
"@workflow/utils": "5.0.1",
|
|
67
67
|
"ms": "2.1.3",
|
|
68
|
-
"@workflow/next": "5.0.
|
|
69
|
-
"@workflow/nest": "5.
|
|
70
|
-
"@workflow/nitro": "5.0.
|
|
71
|
-
"@workflow/nuxt": "5.0.
|
|
72
|
-
"@workflow/sveltekit": "5.0.
|
|
73
|
-
"@workflow/rollup": "5.0.
|
|
68
|
+
"@workflow/next": "5.0.2",
|
|
69
|
+
"@workflow/nest": "5.1.0",
|
|
70
|
+
"@workflow/nitro": "5.0.2",
|
|
71
|
+
"@workflow/nuxt": "5.0.2",
|
|
72
|
+
"@workflow/sveltekit": "5.0.2",
|
|
73
|
+
"@workflow/rollup": "5.0.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/ms": "2.1.0",
|