workflow 5.0.0-beta.39 → 5.0.0-beta.40

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/README.md CHANGED
@@ -14,24 +14,83 @@
14
14
 
15
15
  </div>
16
16
 
17
- ## Getting Started
17
+ [Workflow SDK](https://workflow-sdk.dev) makes TypeScript and JavaScript
18
+ functions durable. It persists workflow progress, retries failed steps, and
19
+ provides built-in observability. Workflows can suspend without using compute
20
+ while they wait.
18
21
 
19
- The **Workflow SDK** lets you easily add durability, reliability, and observability to async JavaScript. Build apps and AI agents that can suspend, resume, and maintain state with ease.
22
+ ## Quick start
20
23
 
21
- Visit [https://workflow-sdk.dev](https://workflow-sdk.dev) to view the full documentation.
24
+ Install the SDK in an existing project:
25
+
26
+ ```bash
27
+ npm install workflow
28
+ ```
29
+
30
+ Configure the integration for your framework. For example, with Next.js:
31
+
32
+ ```ts
33
+ // next.config.ts
34
+ import { withWorkflow } from 'workflow/next';
35
+
36
+ export default withWorkflow({});
37
+ ```
38
+
39
+ Then start a workflow from an API route, Server Action, or other server-side
40
+ code:
41
+
42
+ ```ts
43
+ import { start } from 'workflow/api';
44
+ import { onboardUser } from './workflows/onboard-user';
45
+
46
+ await start(onboardUser, ['hello@example.com']);
47
+ ```
48
+
49
+ Run your app, then open the local observability UI in another terminal:
50
+
51
+ ```bash
52
+ npm run dev
53
+ ```
54
+
55
+ ```bash
56
+ npx workflow web
57
+ ```
58
+
59
+ Choose your framework in the
60
+ [getting-started guides](https://workflow-sdk.dev/docs/getting-started).
61
+
62
+ > [!NOTE]
63
+ > The `workflow` package includes its full documentation, so coding agents can
64
+ > read version-matched guides locally from `node_modules/workflow/docs`.
65
+
66
+ ## Run anywhere
67
+
68
+ Local development uses the bundled backend with no configuration. Deploy to
69
+ Vercel for managed storage, queuing, scaling, and observability. To self-host,
70
+ use the Postgres backend or implement a custom
71
+ [World](https://workflow-sdk.dev/docs/deploying).
72
+
73
+ There are many third-party Worlds (both self-hosted or managed), see [the Worlds page](https://workflow-sdk.dev/worlds) for a list of maintainer-curated third party worlds. Submit your world by opening updating the [Worlds Manifest](https://github.com/vercel/workflow/blob/main/worlds-manifest.json).
22
74
 
23
75
  ## Community
24
76
 
25
- The Workflow SDK community can be found on [GitHub Discussions](https://github.com/vercel/workflow/discussions), where you can ask questions, voice ideas, and share your projects with other people.
77
+ The Workflow SDK community lives on
78
+ [GitHub Discussions](https://github.com/vercel/workflow/discussions), where you
79
+ can ask questions, share ideas, and show what you have built.
26
80
 
27
81
  ## Contributing
28
82
 
29
- Contributions to Workflow SDK are welcome and highly appreciated. Please use GitHub [issues](https://github.com/vercel/workflow/issues) and [discussions](https://github.com/vercel/workflow/discussions) to collaborate with the team and wider community.
30
-
31
- ---
83
+ Contributions are welcome. Use
84
+ [issues](https://github.com/vercel/workflow/issues) and
85
+ [discussions](https://github.com/vercel/workflow/discussions) to collaborate
86
+ with the team and wider community. By participating, you agree to our
87
+ [Code of Conduct](https://github.com/vercel/workflow/blob/main/CODE_OF_CONDUCT.md).
32
88
 
33
89
  ## Security
34
90
 
35
91
  If you believe you have found a security vulnerability in Workflow SDK, we encourage you to **_responsibly disclose this and not open a public issue_**.
36
92
 
37
- To participate in our Open Source Software Bug Bounty program, please email [responsible.disclosure@vercel.com](mailto:responsible.disclosure@vercel.com). We will add you to the program and provide further instructions for submitting your report.
93
+ To participate in our Open Source Software Bug Bounty program, please email
94
+ [responsible.disclosure@vercel.com](mailto:responsible.disclosure@vercel.com).
95
+ We will add you to the program and provide further instructions for submitting
96
+ your report.
@@ -187,7 +187,7 @@ After the workflow ends, [`getHookByToken()`](/docs/api-reference/workflow-api/g
187
187
  </Callout>
188
188
 
189
189
  <Callout type="warn">
190
- This option is experimental. If the configured World does not support it, the workflow fails when registering the Hook. `createWebhook()` does not accept this option.
190
+ This option is experimental. Worlds can limit how long tokens are retained; see [World configuration](/docs/configuration/worlds) for each World's limit. If the configured World does not support minimum retention, the workflow fails when registering the Hook. `createWebhook()` does not accept this option.
191
191
  </Callout>
192
192
 
193
193
  ### Waiting for Multiple Payloads
@@ -113,3 +113,28 @@ When using `deploymentId: "latest"`, the workflow run will execute on a potentia
113
113
  - **Workflow identity**: The workflow ID is derived from the function name and file path. If the latest deployment has renamed the workflow function or moved it to a different directory, the workflow ID will no longer match and the run will fail to start.
114
114
  - **Input and output compatibility**: The arguments passed to `start()` are serialized by the calling deployment but deserialized by the target deployment. Similarly, the workflow's return value is serialized by the target deployment but deserialized by the caller. If the workflow's expected arguments or return type have changed (e.g. added required fields, removed fields, or changed types), the run may fail or behave unexpectedly. Ensure that input and output schemas remain backward-compatible across deployments.
115
115
  </Callout>
116
+
117
+ ### Inside a Workflow Function
118
+
119
+ `start()` can be called directly from a workflow function to spawn a child run. It is step-backed, so the spawn records a deterministic step boundary in the parent's event log.
120
+
121
+ ```typescript
122
+ import { start } from "workflow/api";
123
+ import { childWorkflow } from "./workflows/child";
124
+
125
+ export async function parentWorkflow(value: number) {
126
+ "use workflow";
127
+
128
+ const childRun = await start(childWorkflow, [value]); // [!code highlight]
129
+ const result = await childRun.returnValue; // [!code highlight]
130
+ return { childRunId: childRun.runId, result };
131
+ }
132
+ ```
133
+
134
+ <Callout type="info">
135
+ The returned `Run` object is fully functional inside a workflow. Each property access or method call (`.status`, `.returnValue`, `.cancel()`) executes as a separate step. See [Workflow Composition](/cookbook/common-patterns/workflow-composition) for choosing between spawning a child run and awaiting a workflow function directly.
136
+ </Callout>
137
+
138
+ <Callout type="warn">
139
+ `returnValue` polls the child run every second and holds the polling step's worker slot open for as long as the child takes to finish. For long-running children, spawn without awaiting `returnValue` and have the child resume a [hook](/docs/foundations/hooks) when it completes — see the [`startAndWait()` pattern](/cookbook/advanced/child-workflows).
140
+ </Callout>
@@ -111,7 +111,7 @@ Run-scoped listings mirroring their [Storage](/docs/api-reference/workflow-runti
111
111
  ```typescript lineNumbers
112
112
  const steps = await world.analytics.steps.list({ runId });
113
113
  const events = await world.analytics.events.list({ runId, eventType: "step_failed" });
114
- const related = await world.analytics.events.listByCorrelationId({ correlationId });
114
+ const related = await world.analytics.events.listByCorrelationId({ runId, correlationId });
115
115
  const hooks = await world.analytics.hooks.list({ runId });
116
116
  const waits = await world.analytics.waits.list({ runId, status: "waiting" });
117
117
  ```
@@ -96,16 +96,20 @@ const result = await world.events.list({ runId, pagination: { cursor } }); // [!
96
96
 
97
97
  ### events.listByCorrelationId()
98
98
 
99
- List events that share a correlation ID, useful for tracing related events across runs.
99
+ List one run's events that share a correlation ID, useful for tracing a single step, hook or wait through its lifecycle.
100
+
101
+ A correlation ID is unique within its run, not across runs: two runs can each hold a `step_…`, `hook_…` or `wait_…` ID that reads the same. `runId` is therefore required, and it is also what makes the pagination cursor unambiguous.
100
102
 
101
103
  ```typescript lineNumbers
102
104
  const result = await world.events.listByCorrelationId({ // [!code highlight]
105
+ runId,
103
106
  correlationId: "order-123",
104
107
  }); // [!code highlight]
105
108
  ```
106
109
 
107
110
  | Parameter | Type | Description |
108
111
  |-----------|------|-------------|
112
+ | `params.runId` | `string` | The run the correlation ID belongs to |
109
113
  | `params.correlationId` | `string` | The correlation ID to filter by |
110
114
  | `params.pagination.cursor` | `string` | Cursor for the next page |
111
115
 
@@ -159,6 +163,8 @@ const result = await world.runs.list({ // [!code highlight]
159
163
 
160
164
  To cancel a run, create a `run_cancelled` event via `world.events.create()` (see [world.events](#worldevents) above), or use the CLI or Web UI helpers.
161
165
 
166
+ To cancel a batch in one call, a world may implement the optional `runs.cancelMany({ runIds })`. It returns a summary plus a per-run outcome (`cancelled`, `already_cancelled`, `not_cancellable`, `not_found`, or `failed`). Backends that omit it fall back to per-run cancellation automatically.
167
+
162
168
  ### WorkflowRun Type
163
169
 
164
170
  | Field | Type | Description |
@@ -69,6 +69,7 @@ This method runs inside the workflow context and is subject to the same constrai
69
69
  - No Node.js-specific APIs (like `fs`, `path`, `crypto`, etc.)
70
70
  - No non-deterministic operations (like `Math.random()` or `Date.now()`)
71
71
  - No external network calls
72
+ - No side effects on workflow state — the method may run outside deterministic replay, so mutations would not be reconstructed
72
73
 
73
74
  Keep this method simple and focused on extracting data from the instance.
74
75
  </Callout>
@@ -2,7 +2,7 @@
2
2
  title: CLI and Web UI
3
3
  description: CLI flags and environment variables for inspecting local, Postgres, and Vercel Workflow runs.
4
4
  type: reference
5
- summary: Configure workflow inspect, workflow web, workflow health, and observability tooling.
5
+ summary: Configure workflow inspect, workflow cancel, workflow web, workflow health, and observability tooling.
6
6
  related:
7
7
  - /docs/observability
8
8
  - /docs/configuration/worlds
@@ -115,6 +115,34 @@ Vercel project and auth settings can often be inferred from `.vercel/project.jso
115
115
  - Default: disabled
116
116
  - Enables keyboard-controlled pagination for supported list commands.
117
117
 
118
+ ## Bulk cancel
119
+
120
+ `workflow cancel <run-id>` cancels one run. Given a filter instead, it bulk-cancels a batch; bulk mode requires `--status` or `--workflowName`.
121
+
122
+ ### `--status`
123
+
124
+ - Command: `workflow cancel`
125
+ - Default: unset
126
+ - Restricts the batch to this status. Only `pending` and `running` are accepted — terminal runs cannot be cancelled.
127
+
128
+ ### `--workflowName` / `-n`
129
+
130
+ - Command: `workflow cancel`
131
+ - Default: unset
132
+ - Restricts the batch to one workflow. Expects the generated workflow ID from `workflow inspect runs`, not the short function name.
133
+
134
+ ### `--limit` (cancel)
135
+
136
+ - Command: `workflow cancel`
137
+ - Default: `50`
138
+ - Maximum runs to cancel in one batch (1–500). Only one batch is cancelled per invocation; re-run to cancel the next.
139
+
140
+ ### `--confirm` / `-y`
141
+
142
+ - Command: `workflow cancel`
143
+ - Default: disabled
144
+ - Skips the interactive confirmation prompt.
145
+
118
146
  ## Health checks
119
147
 
120
148
  ### `--port` / `-p`
@@ -10,6 +10,16 @@ related:
10
10
 
11
11
  Runtime variables are read where workflows execute. Set them on the deployment or dev server.
12
12
 
13
+ ## Client polling
14
+
15
+ ### `WORKFLOW_RETURN_VALUE_POLL_INTERVAL_MS`
16
+
17
+ - Default: `1000`
18
+ - Minimum: `1`
19
+ - Delay between status requests made by [`Run.returnValue`](/docs/api-reference/workflow-api/get-run) while a workflow run is not yet complete.
20
+ - Increase it to reduce polling traffic at the cost of noticing completion later.
21
+ - This variable is read by the process awaiting `Run.returnValue`, such as an E2E test runner, rather than by the workflow deployment.
22
+
13
23
  ## Replay and queue delivery
14
24
 
15
25
  ### `WORKFLOW_REPLAY_TIMEOUT_MS`
@@ -45,6 +55,14 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
45
55
  - The runtime falls back to the sequential path automatically when the consumer or backend does not attest dedup support (or the payload is too large to inline on the queue message). On the sequential path the event is written *before* dispatch and its failure fails the resume — the fallback trades that resilience away to stay safe when dedup is not enforced, it does not preserve it.
46
56
  - Set `1` to force the sequential path as a kill switch. The chosen strategy is reported on the resume span as `workflow.hook.resume_strategy`.
47
57
 
58
+ ### `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES`
59
+
60
+ - Default: `3`
61
+ - Times a delivery that reached a deployment other than the one its run is pinned to is re-routed to that deployment before the run is failed with [`DEPLOYMENT_MISMATCH`](/docs/errors/deployment-mismatch).
62
+ - Re-routed deliveries back off exponentially (1s, 2s, 4s). Set to `0` to fail the run on the first misrouted delivery.
63
+ - Only applies to Worlds with atomic, immutable deployments (the Vercel World). A run whose pinned deployment cannot be reached at all fails immediately regardless of this value.
64
+ - Transient or unknown queue publishing failures use normal queue redelivery and do not consume this budget.
65
+
48
66
  ### `WORKFLOW_PRECONDITION_GUARD`
49
67
 
50
68
  - Default: enabled
@@ -75,6 +93,17 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
75
93
  - Delay before a re-invocation caused by a rejected event creation.
76
94
  - Unlike an in-process restart, which re-reads immediately, a re-invocation only happens once the in-process budget failed to catch up — so the delay gives the other writers a moment to quiesce.
77
95
 
96
+ ### `WORKFLOW_PER_KIND_CORRELATION_IDS`
97
+
98
+ - Default: disabled
99
+ - Experimental. Gives each kind of entity a workflow creates — steps, waits, hooks, attribute writes, abort controllers, stream IDs — its own sequence of correlation IDs.
100
+ - With one sequence shared by every kind, an ID is an ordinal over the whole run, so a single extra draw of any kind shifts every ID after it. Two concurrent replays of the same run that disagree about one `sleep()` then assign different IDs to every step that follows, and each writes events the other can neither match nor consume, which fails the run with `CORRUPTED_EVENT_LOG`. Per-kind sequences confine that to the kind that actually differs.
101
+ - IDs remain ordered within a kind, so hooks created by your workflow are still listed in creation order. A hook the runtime creates for you, such as the one backing an abort controller, draws from its own kind and so is listed at an arbitrary position relative to your hooks rather than at its creation position.
102
+ - A run must replay under the scheme that minted its IDs. A replay that switches schemes mid-run assigns IDs its own earlier events do not carry, so it can consume none of them and the run fails.
103
+ - On Vercel, a run keeps replaying on the deployment it started on, so it only ever sees the value baked into that deployment. Changing the setting affects new runs only.
104
+ - Elsewhere — `@workflow/world-postgres`, `@workflow/world-local`, any self-hosted process — nothing pins a run to the code that started it. Turn the setting on during a quiet window with no runs in flight, and roll the new value out to your whole fleet at once: a rolling deploy that leaves both values live replays one run under two schemes concurrently, which is the failure the setting exists to reduce.
105
+ - Set `1` to enable.
106
+
78
107
  ## Inline execution
79
108
 
80
109
  ### `WORKFLOW_V2_TIMEOUT_MS`
@@ -103,6 +132,14 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
103
132
  - Use only when step side effects are idempotent.
104
133
  - Set `0` or `false` to force it off, including the first-delivery fast path used by `WORKFLOW_TURBO`.
105
134
 
135
+ ### `WORKFLOW_RETAINED_VM`
136
+
137
+ - Default: enabled
138
+ - Keeps the suspended workflow VM alive across inline steps within one invocation, so each iteration of the inline loop appends only the newly written events instead of replaying the whole event log in a fresh VM.
139
+ - Suspensions involving hooks, waits, or attributes — and any replay divergence — always fall back to a full replay.
140
+ - Step inputs made of plain data (objects, arrays, primitives) and standard built-ins (`Map`, `Set`, `Date`, `RegExp`, typed arrays, `ArrayBuffer`, `URL`, `Headers`) keep the VM retained. Patching or polyfilling built-in prototypes doesn't change that: serialization never calls them. A boundary falls back to a full replay only when serializing its arguments runs code the workflow controls — a getter, a proxy, a custom class serializer — or computes an `Error`'s stack trace.
141
+ - Set `0` or `false` to replay from scratch in a fresh VM on every iteration.
142
+
106
143
  ### `WORKFLOW_INLINE_OWNERSHIP`
107
144
 
108
145
  - Default: enabled
@@ -116,6 +153,23 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
116
153
  - How long after an inline step's latest `step_started` other invocations assume its owner may still be executing the body. Within the lease they defer the step's backstop message; past it they enqueue immediately.
117
154
  - Raise this on self-hosted multi-instance deployments whose inline steps run longer than the default (the default is sized for Vercel's function duration ceiling).
118
155
 
156
+ ## Workflow VM engine
157
+
158
+ ### `WORKFLOW_VM`
159
+
160
+ - Default: `node`
161
+ - Values: `node` or `quickjs`
162
+ - Selects the sandboxed VM engine that executes workflow functions (`"use workflow"`). Step functions are unaffected — they always run with full Node.js access.
163
+ - `node` (default) runs workflow code in a [`node:vm`](https://nodejs.org/api/vm.html) context.
164
+ - `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** — see 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.
165
+ - Global-surface differences under `quickjs` (workflow functions only — step functions always have full Node.js):
166
+ - `crypto.getRandomValues()` and `crypto.randomUUID()` are provided and deterministic (seeded like the node engine's). All `crypto.subtle.*` methods throw with guidance to move to a step function — including `digest`, which the node engine supports.
167
+ - `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.
168
+ - `WebAssembly` and `Atomics` are not available.
169
+ - `process` exposes only a frozen copy of `env`, matching the node engine.
170
+ - The engine choice is stamped into the run's `executionContext` when the run starts, so a run keeps executing on the engine it started on even if the deployment's `WORKFLOW_VM` changes. Runs without a stamped engine use the handler's `WORKFLOW_VM` value.
171
+ - Unknown values throw at startup.
172
+
119
173
  ## Compression and tracing
120
174
 
121
175
  ### `WORKFLOW_DISABLE_COMPRESSION`
@@ -104,6 +104,13 @@ The Local World is the default outside Vercel and is intended for development.
104
104
  - Default: `true`
105
105
  - Re-enqueues pending and running local runs when the World starts. Set the environment variable to `0` or `false` to skip recovery; the factory option wins when both are set.
106
106
 
107
+ ### `WORKFLOW_LOCAL_HOOK_RETENTION_LIMIT_DAYS`
108
+
109
+ - Factory option: none
110
+ - Default: `30`
111
+ - Maximum [`experimental_minRetention`](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) accepted by the Local World, in days.
112
+ - Set this to the same limit as your production World so oversized values fail during local development.
113
+
107
114
  ### `tag`
108
115
 
109
116
  - Environment variable: none
@@ -160,6 +167,13 @@ The Postgres World is a self-hosted durable backend for long-running server proc
160
167
  - Default: `pg` default
161
168
  - Maximum size of the internal `pg.Pool` when the World creates the pool.
162
169
 
170
+ ### `WORKFLOW_POSTGRES_HOOK_RETENTION_LIMIT_DAYS`
171
+
172
+ - Factory option: none
173
+ - Default: `30`
174
+ - Maximum [`experimental_minRetention`](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) accepted by the Postgres World, in days.
175
+ - Set this to the same limit as your production World so oversized values fail during development.
176
+
163
177
  ### `namespace`
164
178
 
165
179
  - Environment variable fallback: `WORKFLOW_QUEUE_NAMESPACE`
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: deployment-mismatch
3
+ description: A workflow run was delivered to a deployment other than the one it is pinned to.
4
+ type: troubleshooting
5
+ summary: Understand how Workflow recovers from a misrouted delivery, and why a run eventually fails with DEPLOYMENT_MISMATCH.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ related:
9
+ - /docs/foundations/versioning
10
+ - /docs/errors/runtime-decryption-failed
11
+ - /docs/foundations/errors-and-retries
12
+ ---
13
+
14
+ Every run is pinned to a single deployment when it starts. When a queued workflow or step callback is delivered to a **different** deployment, Workflow does not execute it there. Instead it re-routes the message to the deployment the run is pinned to, and only if the run keeps arriving elsewhere does it fail with the `DEPLOYMENT_MISMATCH` classification.
15
+
16
+ This is an SDK/runtime signal, not an error thrown by your workflow code, and it is not catchable inside a workflow function.
17
+
18
+ ## Error Message
19
+
20
+ ```
21
+ Workflow run "wrun_..." is pinned to deployment "dpl_A", but was received by deployment "dpl_B". The runtime re-routed the message to "dpl_A" 3 times and it kept arriving elsewhere, so the run was stopped to protect against code-skew errors. Verify that the run's deployment is still available and that queue callbacks are routed to it.
22
+ ```
23
+
24
+ When the queue definitively reports that the run's deployment cannot be reached — it was deleted, or aged out of its retention window — no re-route is possible and the message omits the re-routing clause. Transient or unknown publishing failures leave the current delivery unacknowledged so the queue can redeliver it; they do not fail the run or consume this recovery budget.
25
+
26
+ ## Why A Run Is Pinned
27
+
28
+ A run's deployment is chosen once, at [`start()`](/docs/api-reference/workflow-api/start):
29
+
30
+ - By default it is the deployment that called `start()` — see [Versioning](/docs/foundations/versioning) for why runs are pinned this way.
31
+ - With `start(workflow, args, { deploymentId })` it is the id you pass, so a run can deliberately target a deployment other than the one that created it.
32
+ - With `deploymentId: "latest"` it is the most recent deployment for the current environment, resolved at start time.
33
+
34
+ Whichever it is, that `deploymentId` is recorded on the run, and every subsequent workflow replay and step execution must happen on that deployment. Continuing on a different one is unsafe:
35
+
36
+ 1. **Code skew.** The workflow and step bundles on the receiving deployment may not match the code that produced the run's recorded history, so replay could diverge or produce incorrect results.
37
+ 2. **Encryption.** Step inputs and other event-log payloads are encrypted with a per-run key derived from the pinned deployment's key material. A different deployment derives the wrong key and cannot decrypt them — previously the source of a confusing [runtime-decryption-failed](/docs/errors/runtime-decryption-failed) that exhausted retries with no clear cause.
38
+
39
+ So the runtime checks the pinned deployment before it executes anything, and `DEPLOYMENT_MISMATCH` names the result — instead of the mismatch surfacing later as an unrelated decryption failure.
40
+
41
+ ## Automatic Recovery
42
+
43
+ A deployment that receives a run it does not own first tries to fix the delivery rather than fail the run:
44
+
45
+ 1. It re-enqueues the message **explicitly addressed** to the run's own deployment. This is strictly better-addressed than the send that misrouted, which inherited the producing deployment's ambient id.
46
+ 2. Delivery is delayed with a short exponential backoff (1s, 2s, 4s).
47
+ 3. If the run keeps arriving at the wrong deployment, the run is failed with `DEPLOYMENT_MISMATCH` after `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES` attempts (default `3`). Set it to `0` to fail on the first misrouted delivery instead.
48
+
49
+ Nothing is executed on the wrong deployment during recovery: no workflow code, no step body, no `step_started`, and no hook resume. Whatever the delivery was carrying travels with it, so a pending step keeps its identity and a hook resume keeps its payload — they run on the deployment that can actually decrypt them.
50
+
51
+ Recovery attempts do not create events on the run, so a run that self-heals looks completely normal. They are reported on the invocation's trace span (`workflow.deployment.pinned_id`, `workflow.deployment_mismatch.retry_count`, `workflow.deployment_mismatch.recovered`) and as a runtime warning in your function logs.
52
+
53
+ ## What To Do
54
+
55
+ - **Re-run from the current deployment.** Trigger the workflow again from your latest deployment (or use the **Re-run** button in the Workflow Dashboard). The new run is pinned to the current deployment.
56
+ - **Keep a run's deployment available** for the lifetime of that run. A run whose deployment has been deleted or has aged out cannot be resumed and must be re-run — recovery cannot help, so these fail on the first misrouted delivery. This applies to runs started with an explicit `deploymentId` too: pinning a run to an older deployment keeps it dependent on that deployment for its whole lifetime.
57
+ - **Report it** if the pinned deployment was still available. Include both deployment ids and the run id from the error message, plus the trace span attributes above — a run that failed this way despite a reachable target is a routing fault worth investigating rather than something to work around.
58
+
59
+ ## This Error Cannot Be Caught
60
+
61
+ Like other runtime signals, `DEPLOYMENT_MISMATCH` is **not catchable** inside your workflow function — the run is failed before any workflow or step code executes on the receiving deployment. Check the run status from outside instead:
62
+
63
+ ```typescript lineNumbers
64
+ import { getRun } from "workflow/api";
65
+
66
+ const run = getRun("wrun_abc123");
67
+ const status = await run.status;
68
+ if (status === "failed") {
69
+ console.error("Run failed");
70
+ }
71
+ ```
@@ -80,6 +80,35 @@ Most `Run` properties are async getters that return promises. You need to `await
80
80
 
81
81
  ## Common Patterns
82
82
 
83
+ ### Starting Workflows from Workflow Functions
84
+
85
+ You can also call `start()` directly inside workflow functions to spawn child workflows. For choosing between this and awaiting a workflow function directly, see [Workflow Composition](/cookbook/common-patterns/workflow-composition).
86
+
87
+ ```typescript lineNumbers
88
+ import { start } from "workflow/api";
89
+ import { childWorkflow } from "./workflows/child";
90
+
91
+ export async function parentWorkflow(inputValue: number) {
92
+ "use workflow";
93
+
94
+ const childRun = await start(childWorkflow, [inputValue]); // [!code highlight]
95
+
96
+ // childRun is a full Run object — use it like normal
97
+ const childResult = await childRun.returnValue;
98
+ return { childRunId: childRun.runId, childResult };
99
+ }
100
+ ```
101
+
102
+ When `start()` is called inside a workflow function, it automatically executes through an internal step to maintain deterministic replay. The returned `Run` object works just like it does outside workflows — properties like `.runId`, `.status`, `.returnValue`, and methods like `.cancel()` are all available. Each property access or method call executes as a separate step under the hood.
103
+
104
+ <Callout type="info">
105
+ Inside workflow functions, each `Run` property access (e.g., `run.status`, `run.returnValue`) triggers a workflow step. This means each access is recorded in the event log and replayed deterministically.
106
+ </Callout>
107
+
108
+ <Callout type="warn">
109
+ Awaiting `returnValue` polls the child run every second, and the polling step holds its worker slot open for as long as the child takes to finish. Worker-based Worlds must be sized to cover the peak number of these polls in flight. If the child workflow is long-running, spawn it without awaiting `returnValue` and have it resume a [hook](/docs/foundations/hooks) when it completes — see the [`startAndWait()` pattern](/cookbook/advanced/child-workflows).
110
+ </Callout>
111
+
83
112
  ### Fire and Forget
84
113
 
85
114
  The most common pattern is to start a workflow and immediately return, letting it execute in the background:
@@ -148,7 +177,7 @@ export async function POST(request: Request) {
148
177
  }
149
178
  ```
150
179
 
151
- Your workflow can obtain a writable stream using [`getWritable()`](/docs/api-reference/workflow/get-writable):
180
+ Your workflow can write to the stream using [`getWritable()`](/docs/api-reference/workflow/get-writable):
152
181
 
153
182
  ```typescript lineNumbers
154
183
  import { getWritable } from "workflow";
@@ -213,6 +242,50 @@ export async function GET(request: Request) {
213
242
  }
214
243
  ```
215
244
 
245
+ ### Recursive and Repeating Workflows
246
+
247
+ A workflow can start a new instance of itself. This is useful when a single long-running workflow would accumulate too many events — large event logs become slower to replay, more expensive to store, and harder to inspect in the UI. By breaking work into smaller runs that chain together, each run stays lean.
248
+
249
+ ```typescript lineNumbers
250
+ import { start } from "workflow/api";
251
+ declare function fetchBatch(cursor?: string): Promise<{ items: string[]; nextCursor?: string }>; // @setup
252
+ declare function processBatch(items: string[]): Promise<void>; // @setup
253
+
254
+ export async function processQueue(cursor?: string) {
255
+ "use workflow";
256
+
257
+ const { items, nextCursor } = await fetchBatch(cursor);
258
+ await processBatch(items);
259
+
260
+ if (nextCursor) {
261
+ // Continue processing in a new workflow run
262
+ await start(processQueue, [nextCursor]); // [!code highlight]
263
+ }
264
+ }
265
+ ```
266
+
267
+ This pattern also enables **repeating cron-like workflows**. A workflow can complete its work, sleep, and then schedule a new instance of itself — creating an indefinite chain without any single run growing too large:
268
+
269
+ ```typescript lineNumbers
270
+ import { sleep } from "workflow";
271
+ import { start } from "workflow/api";
272
+ declare function refreshMetrics(): Promise<void>; // @setup
273
+
274
+ export async function syncDashboard() {
275
+ "use workflow";
276
+
277
+ await refreshMetrics();
278
+ await sleep("1h");
279
+
280
+ // Schedule the next run
281
+ await start(syncDashboard); // [!code highlight]
282
+ }
283
+ ```
284
+
285
+ #### Starting against the latest deployment
286
+
287
+ By default a chained run starts on the same deployment as its parent. For workflows that chain over long periods, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) so the next run picks up new code. [Versioning](/docs/foundations/versioning#self-upgrading-workflows) covers this pattern in full, including how the serialized state acts as the migration boundary between versions.
288
+
216
289
  ## Next Steps
217
290
 
218
291
  Now that you understand how to start workflows and track their execution:
@@ -42,6 +42,8 @@ npx workflow inspect runs --web
42
42
 
43
43
  ![Workflow SDK Web UI](/o11y-ui.png)
44
44
 
45
+ In the runs table, select one or more runs and choose **Cancel** to cancel the batch in a single request. Runs that fail with a retryable error stay selected so you can retry them.
46
+
45
47
  To share a link to a specific run without opening a browser, use the `--url`
46
48
  flag. It prints the dashboard deep link to stdout and exits (no browser, no
47
49
  local server) — useful for scripts, PR comments, or automation. Add `--json` to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "5.0.0-beta.39",
3
+ "version": "5.0.0-beta.40",
4
4
  "description": "Workflow SDK - Build durable, resilient, and observable workflows",
5
5
  "main": "dist/typescript-plugin.cjs",
6
6
  "type": "module",
@@ -59,18 +59,18 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "ms": "2.1.3",
62
- "@workflow/astro": "5.0.0-beta.39",
63
- "@workflow/cli": "5.0.0-beta.39",
64
- "@workflow/core": "5.0.0-beta.39",
65
- "@workflow/errors": "5.0.0-beta.15",
62
+ "@workflow/astro": "5.0.0-beta.40",
63
+ "@workflow/cli": "5.0.0-beta.40",
64
+ "@workflow/core": "5.0.0-beta.40",
65
+ "@workflow/errors": "5.0.0-beta.16",
66
66
  "@workflow/typescript-plugin": "5.0.0-beta.5",
67
67
  "@workflow/utils": "5.0.0-beta.8",
68
- "@workflow/next": "5.0.0-beta.39",
69
- "@workflow/nest": "5.0.0-beta.39",
70
- "@workflow/nitro": "5.0.0-beta.39",
71
- "@workflow/nuxt": "5.0.0-beta.39",
72
- "@workflow/sveltekit": "5.0.0-beta.39",
73
- "@workflow/rollup": "5.0.0-beta.39"
68
+ "@workflow/next": "5.0.0-beta.40",
69
+ "@workflow/nest": "5.0.0-beta.40",
70
+ "@workflow/nitro": "5.0.0-beta.40",
71
+ "@workflow/nuxt": "5.0.0-beta.40",
72
+ "@workflow/sveltekit": "5.0.0-beta.40",
73
+ "@workflow/rollup": "5.0.0-beta.40"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@types/ms": "2.1.0",