@workflow/core 5.0.0-beta.5 → 5.0.0-beta.7
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/dist/classify-error.d.ts +4 -3
- package/dist/classify-error.d.ts.map +1 -1
- package/dist/classify-error.js +29 -5
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +52 -6
- package/dist/encryption.d.ts +7 -1
- package/dist/encryption.d.ts.map +1 -1
- package/dist/encryption.js +13 -6
- package/dist/runtime/constants.d.ts +47 -0
- package/dist/runtime/constants.d.ts.map +1 -1
- package/dist/runtime/constants.js +86 -8
- package/dist/runtime/helpers.d.ts.map +1 -1
- package/dist/runtime/helpers.js +10 -2
- package/dist/runtime/replay-budget.d.ts +98 -0
- package/dist/runtime/replay-budget.d.ts.map +1 -0
- package/dist/runtime/replay-budget.js +191 -0
- package/dist/runtime/resume-hook.d.ts.map +1 -1
- package/dist/runtime/resume-hook.js +2 -1
- package/dist/runtime/runs.d.ts.map +1 -1
- package/dist/runtime/runs.js +4 -1
- package/dist/runtime/step-executor.d.ts.map +1 -1
- package/dist/runtime/step-executor.js +8 -1
- package/dist/runtime/step-handler.d.ts.map +1 -1
- package/dist/runtime/step-handler.js +10 -1
- package/dist/runtime/suspension-handler.d.ts.map +1 -1
- package/dist/runtime/suspension-handler.js +6 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +288 -174
- package/dist/serialization/reducers/common.d.ts.map +1 -1
- package/dist/serialization/reducers/common.js +26 -2
- package/dist/serialization/types.d.ts +14 -0
- package/dist/serialization/types.d.ts.map +1 -1
- package/dist/serialization/types.js +1 -1
- package/dist/serialization.d.ts +8 -0
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +100 -11
- package/dist/step/writable-stream.d.ts.map +1 -1
- package/dist/step/writable-stream.js +17 -1
- package/dist/step.d.ts.map +1 -1
- package/dist/step.js +15 -4
- package/dist/symbols.d.ts +16 -0
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +17 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/dist/workflow/abort-controller.d.ts.map +1 -1
- package/dist/workflow/abort-controller.js +13 -2
- package/dist/workflow/hook.d.ts.map +1 -1
- package/dist/workflow/hook.js +13 -4
- package/dist/workflow/sleep.d.ts.map +1 -1
- package/dist/workflow/sleep.js +22 -3
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +3 -3
- package/docs/foundations/index.mdx +3 -0
- package/docs/foundations/meta.json +2 -1
- package/docs/foundations/starting-workflows.mdx +5 -1
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/how-it-works/code-transform.mdx +2 -2
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- package/package.json +5 -5
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Versioning
|
|
3
|
+
description: Understand how workflow runs are pinned to deployments, how to recover runs after a fix, and how to opt in to newer code explicitly.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Keep in-flight runs stable by default, then choose explicit upgrade boundaries when you need them.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/starting-workflows
|
|
8
|
+
related:
|
|
9
|
+
- /docs/api-reference/workflow-api/start
|
|
10
|
+
- /docs/foundations/cancellation
|
|
11
|
+
- /cookbook/common-patterns/workflow-composition
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Workflow runs are pinned to the deployment that starts them. When a run begins, Workflow SDK records the deployment for that run and continues executing the run on that same copy of your code.
|
|
15
|
+
|
|
16
|
+
That default is intentional. Durable workflows can pause for minutes, days, or months. If the code underneath a paused run changed every time you deployed, an in-flight run could resume into a different function body, different step names, or different input types than the ones it started with. That can make type safety fragile and can break long-running work in hard-to-debug ways.
|
|
17
|
+
|
|
18
|
+
With Workflow SDK, you can keep shipping. New runs use new deployments, while existing runs keep the version they already understand.
|
|
19
|
+
|
|
20
|
+
## Default behavior
|
|
21
|
+
|
|
22
|
+
Start a workflow normally:
|
|
23
|
+
|
|
24
|
+
```typescript title="app/api/orders/route.ts" lineNumbers
|
|
25
|
+
import { start } from "workflow/api";
|
|
26
|
+
import { fulfillOrder } from "@/workflows/fulfill-order";
|
|
27
|
+
|
|
28
|
+
export async function POST(request: Request) {
|
|
29
|
+
const { orderId } = await request.json();
|
|
30
|
+
|
|
31
|
+
const run = await start(fulfillOrder, [orderId]); // [!code highlight]
|
|
32
|
+
|
|
33
|
+
return Response.json({ runId: run.runId });
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The run is tied to the deployment that handled this request. If you deploy a new version while the workflow is [sleeping](/docs/api-reference/workflow/sleep), [waiting on a hook](/docs/foundations/hooks), [retrying a step](/docs/foundations/errors-and-retries), or processing later queue messages, that existing run still resumes on the original deployment.
|
|
38
|
+
|
|
39
|
+
```typescript title="workflows/fulfill-order.ts" lineNumbers
|
|
40
|
+
import { sleep } from "workflow";
|
|
41
|
+
|
|
42
|
+
export async function fulfillOrder(orderId: string) {
|
|
43
|
+
"use workflow";
|
|
44
|
+
|
|
45
|
+
await reserveInventory(orderId);
|
|
46
|
+
await sleep("2d");
|
|
47
|
+
await chargeCustomer(orderId);
|
|
48
|
+
await shipOrder(orderId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function reserveInventory(orderId: string) {
|
|
52
|
+
"use step";
|
|
53
|
+
// ...
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function chargeCustomer(orderId: string) {
|
|
57
|
+
"use step";
|
|
58
|
+
// ...
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function shipOrder(orderId: string) {
|
|
62
|
+
"use step";
|
|
63
|
+
// ...
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you deploy a change to `chargeCustomer()` while a run is in the two-day sleep, the existing run does not suddenly resume into the new implementation. It continues on the deployment it started on. The next order starts on the latest deployment and uses the new code from the beginning.
|
|
68
|
+
|
|
69
|
+
## Fixing in-flight runs
|
|
70
|
+
|
|
71
|
+
Sometimes you deploy because the old code had a bug. The safest fix is usually explicit:
|
|
72
|
+
|
|
73
|
+
1. Deploy the fixed code.
|
|
74
|
+
2. Find the affected runs in [observability](/docs/observability) or with the CLI.
|
|
75
|
+
3. Cancel the old runs if they are still running.
|
|
76
|
+
4. Rerun them on the latest deployment with the same inputs.
|
|
77
|
+
|
|
78
|
+
This keeps the version boundary visible. The old run ends as cancelled or failed, and the replacement run starts fresh on the fixed deployment. This is a good fit for one-off, ad-hoc upgrades where you explicitly opt in to moving affected runs onto a new version.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Inspect affected runs and copy the exact workflowName value.
|
|
82
|
+
npx workflow inspect runs \
|
|
83
|
+
--backend vercel \
|
|
84
|
+
--status running
|
|
85
|
+
|
|
86
|
+
# Cancel one run.
|
|
87
|
+
npx workflow cancel <run-id> \
|
|
88
|
+
--backend vercel
|
|
89
|
+
|
|
90
|
+
# Or bulk-cancel matching running runs.
|
|
91
|
+
npx workflow cancel \
|
|
92
|
+
--status running \
|
|
93
|
+
--workflowName "workflow//./workflows/fulfill-order//fulfillOrder" \
|
|
94
|
+
--backend vercel
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-api/world/observability) when you need display-friendly names.
|
|
98
|
+
|
|
99
|
+
In the [observability UI](/docs/observability), use **Rerun on latest** to enqueue the workflow again with the same inputs against the latest deployment.
|
|
100
|
+
|
|
101
|
+
If you are writing your own recovery route, call `start()` with the same arguments and `deploymentId: "latest"`:
|
|
102
|
+
|
|
103
|
+
```typescript title="app/api/orders/rerun/route.ts" lineNumbers
|
|
104
|
+
import { start } from "workflow/api";
|
|
105
|
+
import { fulfillOrder } from "@/workflows/fulfill-order";
|
|
106
|
+
|
|
107
|
+
export async function POST(request: Request) {
|
|
108
|
+
const { orderId } = await request.json();
|
|
109
|
+
|
|
110
|
+
const run = await start(fulfillOrder, [orderId], {
|
|
111
|
+
deploymentId: "latest", // [!code highlight]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return Response.json({ runId: run.runId });
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
<Callout type="warn">
|
|
119
|
+
`deploymentId: "latest"` is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment. Because the caller and target deployment can be different, keep the [workflow function name and file path](/docs/errors/workflow-not-registered), arguments, and return value backward-compatible across the deployments you plan to bridge.
|
|
120
|
+
</Callout>
|
|
121
|
+
|
|
122
|
+
## Self upgrading workflows
|
|
123
|
+
|
|
124
|
+
Some workflows are expected to run for a very long time. Scheduled loops, recurring jobs, agents, and chat sessions often should not stay on one deployment forever.
|
|
125
|
+
|
|
126
|
+
Model those as a sequence of runs. Each run does a bounded piece of work, then starts the next run on the latest deployment and exits. This is similar to `continueAsNew` in other durable execution systems, but in Workflow SDK it is just [explicit recursion through `start()`](/cookbook/common-patterns/workflow-composition).
|
|
127
|
+
|
|
128
|
+
```typescript title="workflows/daily-digest.ts" lineNumbers
|
|
129
|
+
import { sleep } from "workflow";
|
|
130
|
+
import { start } from "workflow/api";
|
|
131
|
+
|
|
132
|
+
type DigestState = {
|
|
133
|
+
userId: string;
|
|
134
|
+
lastSentAt?: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export async function dailyDigest(state: DigestState) {
|
|
138
|
+
"use workflow";
|
|
139
|
+
|
|
140
|
+
const sentAt = await sendDigest(state.userId);
|
|
141
|
+
await sleep("1d");
|
|
142
|
+
|
|
143
|
+
const run = await start(
|
|
144
|
+
dailyDigest,
|
|
145
|
+
[{ ...state, lastSentAt: sentAt }],
|
|
146
|
+
{
|
|
147
|
+
deploymentId: "latest", // [!code highlight]
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { continuedAs: run.runId };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function sendDigest(userId: string) {
|
|
155
|
+
"use step";
|
|
156
|
+
// ...
|
|
157
|
+
return new Date().toISOString();
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This pattern gives every run a clear lifecycle:
|
|
162
|
+
|
|
163
|
+
- The current run stays on its original deployment.
|
|
164
|
+
- The next run starts on the latest deployment.
|
|
165
|
+
- The [serialized `state`](/docs/foundations/serialization) is the migration boundary between versions.
|
|
166
|
+
- Observability can link parent and child runs when a workflow starts another run.
|
|
167
|
+
|
|
168
|
+
## Carrying context forward
|
|
169
|
+
|
|
170
|
+
Anything that is [serializable by Workflow SDK](/docs/foundations/serialization) can be passed from one run to the next as an argument. That includes plain state objects, `ReadableStream`, `WritableStream`, `AbortSignal`, and other supported serialized values.
|
|
171
|
+
|
|
172
|
+
For example, a long export can register its [output stream](/docs/foundations/streaming) once, write progress from each run, and pass the same stream plus updated state into the next run:
|
|
173
|
+
|
|
174
|
+
```typescript title="workflows/export-report.ts" lineNumbers
|
|
175
|
+
import { getWritable } from "workflow";
|
|
176
|
+
import { start } from "workflow/api";
|
|
177
|
+
|
|
178
|
+
type ExportState = {
|
|
179
|
+
exportId: string;
|
|
180
|
+
page: number;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export async function exportReport(
|
|
184
|
+
state: ExportState,
|
|
185
|
+
progress?: WritableStream<string>
|
|
186
|
+
) {
|
|
187
|
+
"use workflow";
|
|
188
|
+
|
|
189
|
+
// Register the stream once. Continuation runs receive this same stream
|
|
190
|
+
// as an argument and keep writing to it.
|
|
191
|
+
const stream =
|
|
192
|
+
progress !== undefined ? progress : getWritable<string>();
|
|
193
|
+
|
|
194
|
+
const hasMore = await exportPage(state, stream);
|
|
195
|
+
|
|
196
|
+
if (!hasMore) {
|
|
197
|
+
await writeProgress(stream, { type: "done", totalPages: state.page });
|
|
198
|
+
return { totalPages: state.page };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const run = await start(exportReport, [
|
|
202
|
+
{ ...state, page: state.page + 1 },
|
|
203
|
+
stream,
|
|
204
|
+
], {
|
|
205
|
+
deploymentId: "latest", // [!code highlight]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { continuedAs: run.runId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function exportPage(
|
|
212
|
+
state: ExportState,
|
|
213
|
+
stream: WritableStream<string>
|
|
214
|
+
) {
|
|
215
|
+
"use step";
|
|
216
|
+
|
|
217
|
+
// Do work for this version boundary.
|
|
218
|
+
const hasMore = state.page < 10;
|
|
219
|
+
const writer = stream.getWriter();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await writer.write(
|
|
223
|
+
JSON.stringify({ type: "page", page: state.page }) + "\n"
|
|
224
|
+
);
|
|
225
|
+
return hasMore;
|
|
226
|
+
} finally {
|
|
227
|
+
writer.releaseLock();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function writeProgress(
|
|
232
|
+
stream: WritableStream<string>,
|
|
233
|
+
event: { type: "done"; totalPages: number }
|
|
234
|
+
) {
|
|
235
|
+
"use step";
|
|
236
|
+
|
|
237
|
+
const writer = stream.getWriter();
|
|
238
|
+
try {
|
|
239
|
+
await writer.write(JSON.stringify(event) + "\n");
|
|
240
|
+
} finally {
|
|
241
|
+
writer.releaseLock();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```typescript title="app/api/export/route.ts" lineNumbers
|
|
247
|
+
import { start } from "workflow/api";
|
|
248
|
+
import { exportReport } from "@/workflows/export-report";
|
|
249
|
+
|
|
250
|
+
export async function POST(request: Request) {
|
|
251
|
+
const { exportId } = await request.json();
|
|
252
|
+
|
|
253
|
+
const run = await start(exportReport, [{ exportId, page: 1 }]);
|
|
254
|
+
|
|
255
|
+
// Linked continuation runs keep writing to the stream registered by
|
|
256
|
+
// the parent run, because that stream is passed forward as an argument.
|
|
257
|
+
return new Response(run.readable, {
|
|
258
|
+
headers: { "Content-Type": "application/jsonl" },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Each run still has one clear version boundary: the current run stays on its original deployment, the next run starts on the latest deployment, and only the explicit state and stream handle are carried forward.
|
|
@@ -193,7 +193,7 @@ handleUserSignup.workflowId = "workflow//workflows/user.js//handleUserSignup"; /
|
|
|
193
193
|
- The `workflowId` property is added (same as workflow mode)
|
|
194
194
|
- Step functions are not transformed in client mode
|
|
195
195
|
|
|
196
|
-
**Why this transformation?** Workflow functions cannot be called directly—they must be started using [`start()`](/docs/api-reference/workflow-api/start). The error prevents accidental direct execution while the `workflowId` property allows the `start()` function to identify which workflow to launch.
|
|
196
|
+
**Why this transformation?** Workflow functions cannot be called directly from application code—they must be started using [`start()`](/docs/api-reference/workflow-api/start). The error prevents accidental direct execution while the `workflowId` property allows the `start()` function to identify which workflow to launch.
|
|
197
197
|
|
|
198
198
|
The IDs are generated exactly like in workflow mode to ensure they can be directly referenced at runtime.
|
|
199
199
|
|
|
@@ -321,7 +321,7 @@ The compiler generates stable IDs for workflows and steps based on file paths an
|
|
|
321
321
|
- **Portable**: Works across different runtimes and deployments
|
|
322
322
|
|
|
323
323
|
<Callout type="info">
|
|
324
|
-
Although IDs can change when files are moved or functions are renamed, Workflow SDK
|
|
324
|
+
Although IDs can change when files are moved or functions are renamed, Workflow SDK functions assume [atomic versioning](/docs/foundations/versioning) in the World. This means changing IDs won't break old workflows from running, but will prevent runs from being upgraded and will cause your workflow/step names to change in observability across deployments.
|
|
325
325
|
</Callout>
|
|
326
326
|
|
|
327
327
|
## Framework Integration
|
|
@@ -127,7 +127,7 @@ flowchart TD
|
|
|
127
127
|
|
|
128
128
|
Unlike other entities, hooks don't have a `status` field—the states above are conceptual. An "active" hook is one that exists in storage, while "disposed" means the hook has been deleted. When a `hook_disposed` event is created, the hook record is removed rather than updated.
|
|
129
129
|
|
|
130
|
-
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
|
|
130
|
+
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. Current worlds include the token and the run ID that currently owns it, though older persisted events or world implementations may only include the token. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
|
|
131
131
|
|
|
132
132
|
When a hook is disposed (either explicitly or when its workflow completes), the token is released and can be claimed by future workflows. Hooks are automatically disposed when a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`). The `hook_disposed` event is only needed for explicit disposal before workflow completion.
|
|
133
133
|
|
|
@@ -188,7 +188,7 @@ Events are categorized by the entity type they affect. Each event contains metad
|
|
|
188
188
|
| Event | Description |
|
|
189
189
|
|-------|-------------|
|
|
190
190
|
| `hook_created` | Creates a new hook in `active` state. Contains the hook token and optional metadata. |
|
|
191
|
-
| `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
|
|
191
|
+
| `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. Contains the token and, for current worlds, the active hook owner's run ID. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
|
|
192
192
|
| `hook_received` | Records that a payload was delivered to the hook. The hook remains `active` and can receive more payloads. |
|
|
193
193
|
| `hook_disposed` | Deletes the hook from storage (conceptually transitioning to `disposed` state). The token is released for reuse by future workflows. |
|
|
194
194
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow/core",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.7",
|
|
4
4
|
"description": "Core runtime and engine for Workflow SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"semver": "7.7.4",
|
|
90
90
|
"ulid": "~3.0.1",
|
|
91
91
|
"zod": "4.3.6",
|
|
92
|
-
"@workflow/errors": "5.0.0-beta.
|
|
92
|
+
"@workflow/errors": "5.0.0-beta.4",
|
|
93
93
|
"@workflow/serde": "5.0.0-beta.1",
|
|
94
94
|
"@workflow/utils": "5.0.0-beta.2",
|
|
95
|
-
"@workflow/world": "5.0.0-beta.
|
|
96
|
-
"@workflow/world-local": "5.0.0-beta.
|
|
97
|
-
"@workflow/world-vercel": "5.0.0-beta.
|
|
95
|
+
"@workflow/world": "5.0.0-beta.4",
|
|
96
|
+
"@workflow/world-local": "5.0.0-beta.6",
|
|
97
|
+
"@workflow/world-vercel": "5.0.0-beta.6"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@opentelemetry/api": "1.9.0",
|