@workflow/core 5.0.0-beta.5 → 5.0.0-beta.51
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 +5 -0
- package/dist/attribute-changes.d.ts +5 -0
- package/dist/attribute-changes.d.ts.map +1 -0
- package/dist/attribute-changes.js +25 -0
- package/dist/capabilities.d.ts +35 -2
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +74 -5
- package/dist/capture-stack.d.ts +1 -1
- package/dist/capture-stack.js +2 -2
- package/dist/class-serialization.d.ts +32 -0
- package/dist/class-serialization.d.ts.map +1 -1
- package/dist/class-serialization.js +37 -1
- package/dist/classify-error.d.ts +19 -3
- package/dist/classify-error.d.ts.map +1 -1
- package/dist/classify-error.js +98 -6
- package/dist/context-violation-error.d.ts +5 -5
- package/dist/context-violation-error.d.ts.map +1 -1
- package/dist/context-violation-error.js +8 -6
- package/dist/create-hook.d.ts +72 -7
- package/dist/create-hook.d.ts.map +1 -1
- package/dist/create-hook.js +1 -1
- package/dist/define-hook.js +2 -2
- package/dist/describe-error.d.ts +7 -7
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +97 -12
- package/dist/encryption.d.ts +38 -4
- package/dist/encryption.d.ts.map +1 -1
- package/dist/encryption.js +96 -14
- package/dist/events-consumer.d.ts +297 -0
- package/dist/events-consumer.d.ts.map +1 -1
- package/dist/events-consumer.js +673 -42
- package/dist/flushable-stream.d.ts +97 -1
- package/dist/flushable-stream.d.ts.map +1 -1
- package/dist/flushable-stream.js +291 -11
- package/dist/global.d.ts +36 -2
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +21 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/log-format.d.ts +2 -2
- package/dist/log-format.js +19 -12
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +10 -6
- package/dist/private.d.ts +226 -9
- package/dist/private.d.ts.map +1 -1
- package/dist/private.js +626 -7
- package/dist/replay-divergence.d.ts +25 -0
- package/dist/replay-divergence.d.ts.map +1 -0
- package/dist/replay-divergence.js +61 -0
- package/dist/replay-payload-cache.d.ts +64 -0
- package/dist/replay-payload-cache.d.ts.map +1 -0
- package/dist/replay-payload-cache.js +158 -0
- package/dist/runtime/compute-instance.d.ts +13 -0
- package/dist/runtime/compute-instance.d.ts.map +1 -0
- package/dist/runtime/compute-instance.js +14 -0
- package/dist/runtime/constants.d.ts +315 -0
- package/dist/runtime/constants.d.ts.map +1 -1
- package/dist/runtime/constants.js +502 -16
- package/dist/runtime/count-step-started-events.d.ts +52 -0
- package/dist/runtime/count-step-started-events.d.ts.map +1 -0
- package/dist/runtime/count-step-started-events.js +72 -0
- package/dist/runtime/deployment-guard.d.ts +99 -0
- package/dist/runtime/deployment-guard.d.ts.map +1 -0
- package/dist/runtime/deployment-guard.js +154 -0
- package/dist/runtime/get-port-lazy.d.ts +15 -0
- package/dist/runtime/get-port-lazy.d.ts.map +1 -1
- package/dist/runtime/get-port-lazy.js +75 -6
- package/dist/runtime/get-world-lazy.d.ts +5 -14
- package/dist/runtime/get-world-lazy.d.ts.map +1 -1
- package/dist/runtime/get-world-lazy.js +19 -29
- package/dist/runtime/helpers.d.ts +415 -29
- package/dist/runtime/helpers.d.ts.map +1 -1
- package/dist/runtime/helpers.js +686 -57
- package/dist/runtime/quickjs-assets.generated.d.ts +14 -0
- package/dist/runtime/quickjs-assets.generated.d.ts.map +1 -0
- package/dist/runtime/quickjs-assets.generated.js +30 -0
- package/dist/runtime/quickjs-entrypoint.d.ts +158 -0
- package/dist/runtime/quickjs-entrypoint.d.ts.map +1 -0
- package/dist/runtime/quickjs-entrypoint.js +1863 -0
- package/dist/runtime/quickjs-log-view.d.ts +129 -0
- package/dist/runtime/quickjs-log-view.d.ts.map +1 -0
- package/dist/runtime/quickjs-log-view.js +229 -0
- package/dist/runtime/quickjs-runtime.d.ts +244 -0
- package/dist/runtime/quickjs-runtime.d.ts.map +1 -0
- package/dist/runtime/quickjs-runtime.js +2427 -0
- package/dist/runtime/quickjs-serde.d.ts +107 -0
- package/dist/runtime/quickjs-serde.d.ts.map +1 -0
- package/dist/runtime/quickjs-serde.js +2159 -0
- package/dist/runtime/replay-budget.d.ts +97 -0
- package/dist/runtime/replay-budget.d.ts.map +1 -0
- package/dist/runtime/replay-budget.js +139 -0
- package/dist/runtime/replay-recovery-reporter.d.ts +36 -0
- package/dist/runtime/replay-recovery-reporter.d.ts.map +1 -0
- package/dist/runtime/replay-recovery-reporter.js +64 -0
- package/dist/runtime/resume-hook.d.ts +91 -8
- package/dist/runtime/resume-hook.d.ts.map +1 -1
- package/dist/runtime/resume-hook.js +512 -89
- package/dist/runtime/resume-latency.d.ts +150 -0
- package/dist/runtime/resume-latency.d.ts.map +1 -0
- package/dist/runtime/resume-latency.js +155 -0
- package/dist/runtime/run-id-time.d.ts +19 -0
- package/dist/runtime/run-id-time.d.ts.map +1 -0
- package/dist/runtime/run-id-time.js +42 -0
- package/dist/runtime/run.d.ts +72 -7
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +280 -53
- package/dist/runtime/runs.d.ts +54 -3
- package/dist/runtime/runs.d.ts.map +1 -1
- package/dist/runtime/runs.js +126 -16
- package/dist/runtime/start.d.ts +101 -2
- package/dist/runtime/start.d.ts.map +1 -1
- package/dist/runtime/start.js +352 -61
- package/dist/runtime/step-executor.d.ts +186 -5
- package/dist/runtime/step-executor.d.ts.map +1 -1
- package/dist/runtime/step-executor.js +768 -130
- package/dist/runtime/step-latency.d.ts +220 -0
- package/dist/runtime/step-latency.d.ts.map +1 -0
- package/dist/runtime/step-latency.js +215 -0
- package/dist/runtime/step-ownership.d.ts +72 -0
- package/dist/runtime/step-ownership.d.ts.map +1 -0
- package/dist/runtime/step-ownership.js +114 -0
- package/dist/runtime/step-single-flight.d.ts +12 -0
- package/dist/runtime/step-single-flight.d.ts.map +1 -0
- package/dist/runtime/step-single-flight.js +75 -0
- package/dist/runtime/suspension-handler.d.ts +247 -7
- package/dist/runtime/suspension-handler.d.ts.map +1 -1
- package/dist/runtime/suspension-handler.js +1304 -125
- package/dist/runtime/unserializable-step.d.ts +34 -0
- package/dist/runtime/unserializable-step.d.ts.map +1 -0
- package/dist/runtime/unserializable-step.js +52 -0
- package/dist/runtime/vm-mode.d.ts +44 -0
- package/dist/runtime/vm-mode.d.ts.map +1 -0
- package/dist/runtime/vm-mode.js +62 -0
- package/dist/runtime/vm-serde-bundle.generated.d.ts +14 -0
- package/dist/runtime/vm-serde-bundle.generated.d.ts.map +1 -0
- package/dist/runtime/vm-serde-bundle.generated.js +16 -0
- package/dist/runtime/wait-continuation.d.ts +106 -0
- package/dist/runtime/wait-continuation.d.ts.map +1 -0
- package/dist/runtime/wait-continuation.js +136 -0
- package/dist/runtime/wait-until.d.ts +18 -0
- package/dist/runtime/wait-until.d.ts.map +1 -0
- package/dist/runtime/wait-until.js +42 -0
- package/dist/runtime/world-compatibility.d.ts +32 -0
- package/dist/runtime/world-compatibility.d.ts.map +1 -0
- package/dist/runtime/world-compatibility.js +44 -0
- package/dist/runtime/world-init.d.ts +17 -42
- package/dist/runtime/world-init.d.ts.map +1 -1
- package/dist/runtime/world-init.js +18 -43
- package/dist/runtime/world.d.ts +14 -2
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +75 -19
- package/dist/runtime.d.ts +10 -6
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +3911 -489
- package/dist/sealed-box.d.ts +167 -0
- package/dist/sealed-box.d.ts.map +1 -0
- package/dist/sealed-box.js +571 -0
- package/dist/serialization/client.d.ts +3 -3
- package/dist/serialization/client.d.ts.map +1 -1
- package/dist/serialization/client.js +8 -4
- package/dist/serialization/codec-devalue-vm.d.ts +16 -0
- package/dist/serialization/codec-devalue-vm.d.ts.map +1 -0
- package/dist/serialization/codec-devalue-vm.js +148 -0
- package/dist/serialization/codec-devalue.d.ts +1 -1
- package/dist/serialization/codec-devalue.d.ts.map +1 -1
- package/dist/serialization/codec-devalue.js +24 -6
- package/dist/serialization/codec.d.ts +37 -2
- package/dist/serialization/codec.d.ts.map +1 -1
- package/dist/serialization/codec.js +2 -2
- package/dist/serialization/compression.d.ts +105 -0
- package/dist/serialization/compression.d.ts.map +1 -0
- package/dist/serialization/compression.js +261 -0
- package/dist/serialization/encryption.d.ts +119 -17
- package/dist/serialization/encryption.d.ts.map +1 -1
- package/dist/serialization/encryption.js +152 -17
- package/dist/serialization/errors.d.ts +13 -0
- package/dist/serialization/errors.d.ts.map +1 -1
- package/dist/serialization/errors.js +20 -2
- package/dist/serialization/format.d.ts +6 -6
- package/dist/serialization/format.js +7 -7
- package/dist/serialization/hardened.d.ts +156 -0
- package/dist/serialization/hardened.d.ts.map +1 -0
- package/dist/serialization/hardened.js +538 -0
- package/dist/serialization/index.d.ts +10 -8
- package/dist/serialization/index.d.ts.map +1 -1
- package/dist/serialization/index.js +10 -8
- package/dist/serialization/reducers/class-vm.d.ts +20 -0
- package/dist/serialization/reducers/class-vm.d.ts.map +1 -0
- package/dist/serialization/reducers/class-vm.js +77 -0
- package/dist/serialization/reducers/class.d.ts.map +1 -1
- package/dist/serialization/reducers/class.js +11 -6
- package/dist/serialization/reducers/common-vm.d.ts +15 -0
- package/dist/serialization/reducers/common-vm.d.ts.map +1 -0
- package/dist/serialization/reducers/common-vm.js +616 -0
- package/dist/serialization/reducers/common.d.ts +1 -1
- package/dist/serialization/reducers/common.d.ts.map +1 -1
- package/dist/serialization/reducers/common.js +202 -62
- package/dist/serialization/reducers/step-function-vm.d.ts +44 -0
- package/dist/serialization/reducers/step-function-vm.d.ts.map +1 -0
- package/dist/serialization/reducers/step-function-vm.js +97 -0
- package/dist/serialization/reducers/step-function.d.ts.map +1 -1
- package/dist/serialization/reducers/step-function.js +25 -9
- package/dist/serialization/step.d.ts +4 -4
- package/dist/serialization/step.d.ts.map +1 -1
- package/dist/serialization/step.js +9 -5
- package/dist/serialization/types.d.ts +80 -1
- package/dist/serialization/types.d.ts.map +1 -1
- package/dist/serialization/types.js +15 -1
- package/dist/serialization/workflow-vm.d.ts +29 -0
- package/dist/serialization/workflow-vm.d.ts.map +1 -0
- package/dist/serialization/workflow-vm.js +74 -0
- package/dist/serialization-format.d.ts +64 -8
- package/dist/serialization-format.d.ts.map +1 -1
- package/dist/serialization-format.js +225 -73
- package/dist/serialization.d.ts +262 -34
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +1885 -277
- package/dist/set-attributes.d.ts +13 -0
- package/dist/set-attributes.d.ts.map +1 -0
- package/dist/set-attributes.js +60 -0
- package/dist/source-map.d.ts +25 -0
- package/dist/source-map.d.ts.map +1 -1
- package/dist/source-map.js +154 -10
- package/dist/step/context-storage.d.ts +63 -2
- package/dist/step/context-storage.d.ts.map +1 -1
- package/dist/step/context-storage.js +7 -5
- package/dist/step/writable-stream.d.ts +3 -3
- package/dist/step/writable-stream.d.ts.map +1 -1
- package/dist/step/writable-stream.js +80 -13
- package/dist/step.d.ts.map +1 -1
- package/dist/step.js +148 -27
- package/dist/symbols.d.ts +50 -0
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +51 -1
- package/dist/telemetry/semantic-conventions.d.ts +433 -2
- package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
- package/dist/telemetry/semantic-conventions.js +319 -3
- package/dist/telemetry.d.ts +91 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +244 -17
- package/dist/test-support/orchestrator-context.d.ts +25 -0
- package/dist/test-support/orchestrator-context.d.ts.map +1 -0
- package/dist/test-support/orchestrator-context.js +107 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +23 -1
- package/dist/util.d.ts +0 -5
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -15
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +2 -2
- package/dist/vm/index.d.ts.map +1 -1
- package/dist/vm/index.js +84 -14
- package/dist/vm/script-cache.d.ts +26 -0
- package/dist/vm/script-cache.d.ts.map +1 -0
- package/dist/vm/script-cache.js +131 -0
- package/dist/vm/uint8array-base64.js +1 -1
- package/dist/workflow/abort-controller.d.ts +2 -2
- package/dist/workflow/abort-controller.d.ts.map +1 -1
- package/dist/workflow/abort-controller.js +88 -21
- package/dist/workflow/attribute-dispatcher.d.ts +6 -0
- package/dist/workflow/attribute-dispatcher.d.ts.map +1 -0
- package/dist/workflow/attribute-dispatcher.js +45 -0
- package/dist/workflow/create-hook.d.ts.map +1 -1
- package/dist/workflow/create-hook.js +24 -3
- package/dist/workflow/get-workflow-metadata.js +2 -2
- package/dist/workflow/hook.d.ts.map +1 -1
- package/dist/workflow/hook.js +304 -47
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +2 -1
- package/dist/workflow/set-attributes.d.ts +68 -0
- package/dist/workflow/set-attributes.d.ts.map +1 -0
- package/dist/workflow/set-attributes.js +60 -0
- package/dist/workflow/sleep.d.ts.map +1 -1
- package/dist/workflow/sleep.js +58 -12
- package/dist/workflow/world-init-stub.d.ts +3 -3
- package/dist/workflow/world-init-stub.js +4 -4
- package/dist/workflow.d.ts +95 -3
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +900 -561
- package/docs/api-reference/create-hook.mdx +89 -10
- package/docs/api-reference/create-webhook.mdx +16 -15
- package/docs/api-reference/define-hook.mdx +35 -33
- package/docs/api-reference/fatal-error.mdx +30 -8
- package/docs/api-reference/fetch.mdx +14 -10
- package/docs/api-reference/get-step-metadata.mdx +2 -2
- package/docs/api-reference/get-workflow-metadata.mdx +3 -3
- package/docs/api-reference/get-writable.mdx +7 -7
- package/docs/api-reference/index.mdx +4 -1
- package/docs/api-reference/retryable-error.mdx +1 -1
- package/docs/api-reference/set-attributes.mdx +61 -0
- package/docs/api-reference/sleep.mdx +4 -4
- package/docs/foundations/cancellation.mdx +31 -32
- package/docs/foundations/errors-and-retries.mdx +42 -11
- package/docs/foundations/hooks.mdx +64 -35
- package/docs/foundations/idempotency.mdx +244 -12
- package/docs/foundations/index.mdx +1 -23
- package/docs/foundations/meta.json +2 -1
- package/docs/foundations/serialization.mdx +21 -22
- package/docs/foundations/starting-workflows.mdx +106 -30
- package/docs/foundations/streaming.mdx +108 -60
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/foundations/workflows-and-steps.mdx +9 -9
- package/docs/how-it-works/cancellation.mdx +63 -63
- package/docs/how-it-works/code-transform.mdx +83 -67
- package/docs/how-it-works/encryption.mdx +30 -26
- package/docs/how-it-works/event-sourcing.mdx +125 -34
- package/docs/how-it-works/framework-integrations.mdx +96 -337
- package/docs/how-it-works/understanding-directives.mdx +22 -22
- package/package.json +24 -15
- package/dist/runtime/step-handler.d.ts +0 -2
- package/dist/runtime/step-handler.d.ts.map +0 -1
- package/dist/runtime/step-handler.js +0 -776
|
@@ -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 canceled 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-observability/parse-workflow-name) 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 long periods. 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 Workflow SDK uses [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.
|
|
@@ -14,18 +14,18 @@ import { File, Folder, Files } from "fumadocs-ui/components/files";
|
|
|
14
14
|
|
|
15
15
|
Workflows (a.k.a. *durable functions*) are a programming model for building long-running, stateful application logic that can maintain its execution state across restarts, failures, or user events. Unlike traditional serverless functions that lose all state when they terminate, workflows persist their progress and can resume exactly where they left off.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Workflows let you model complex multi-step processes in code. To do this, we introduce two fundamental entities:
|
|
18
18
|
|
|
19
19
|
1. **Workflow Functions**: Functions that orchestrate/organize steps
|
|
20
20
|
2. **Step Functions**: Functions that carry out the actual work
|
|
21
21
|
|
|
22
|
-
## Workflow
|
|
22
|
+
## Workflow functions
|
|
23
23
|
|
|
24
24
|
*Directive: `"use workflow"`*
|
|
25
25
|
|
|
26
26
|
Workflow functions define the entrypoint of a workflow and organize how step functions are called. This type of function does not have access to the Node.js runtime, and usable `npm` packages are limited.
|
|
27
27
|
|
|
28
|
-
Although this may seem limiting initially, this feature is
|
|
28
|
+
Although this may seem limiting initially, this feature is required to suspend and accurately resume workflow execution.
|
|
29
29
|
|
|
30
30
|
It helps to think of the workflow function less like a full JavaScript runtime and more like "stitching together" various steps using conditionals, loops, try/catch handlers, `Promise.all`, and other language primitives.
|
|
31
31
|
|
|
@@ -51,7 +51,7 @@ Determinism in the workflow is required to resume the workflow from a suspension
|
|
|
51
51
|
|
|
52
52
|
The sandboxed environment that workflows run in already ensures determinism. For instance, `Math.random` and `Date` constructors are fixed in workflow runs, so you are safe to use them, and the framework ensures that the values don't change across replays.
|
|
53
53
|
|
|
54
|
-
## Step
|
|
54
|
+
## Step functions
|
|
55
55
|
|
|
56
56
|
*Directive: `"use step"`*
|
|
57
57
|
|
|
@@ -115,10 +115,10 @@ export async function POST() {
|
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
<Callout type="info">
|
|
118
|
-
|
|
118
|
+
Calling a step function outside a workflow function provides neither retry semantics nor observability. Additionally, certain workflow-specific functions like [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) will throw an error when used inside a step that's called outside a workflow.
|
|
119
119
|
</Callout>
|
|
120
120
|
|
|
121
|
-
### Suspension and
|
|
121
|
+
### Suspension and resumption
|
|
122
122
|
|
|
123
123
|
Workflow functions have the ability to automatically suspend while they wait on asynchronous work. While suspended, the workflow's state is stored via the [event log](/docs/how-it-works/event-sourcing) and no compute resources are used until the workflow resumes execution.
|
|
124
124
|
|
|
@@ -126,7 +126,7 @@ Workflow functions have the ability to automatically suspend while they wait on
|
|
|
126
126
|
|
|
127
127
|
There are multiple ways a workflow can suspend:
|
|
128
128
|
|
|
129
|
-
- Waiting on a step function: the workflow yields while the step runs in the step
|
|
129
|
+
- Waiting on a step function: the workflow yields while the step body runs. The step usually executes inline in the same invocation; when the invocation's inline budget is exhausted or its timeout approaches, the step is handed to the queue and the workflow resumes in a later invocation.
|
|
130
130
|
- Using `sleep()` to pause for some fixed duration.
|
|
131
131
|
- Awaiting on a promise returned by [`createWebhook()`](/docs/api-reference/workflow/create-webhook), which resumes the workflow when an external system passes data into the workflow.
|
|
132
132
|
|
|
@@ -150,9 +150,9 @@ export async function documentReviewProcess(userId: string) {
|
|
|
150
150
|
}
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
## Writing
|
|
153
|
+
## Writing workflows
|
|
154
154
|
|
|
155
|
-
### Basic
|
|
155
|
+
### Basic structure
|
|
156
156
|
|
|
157
157
|
The simplest workflow consists of a workflow function and one or more step functions.
|
|
158
158
|
|
|
@@ -18,30 +18,30 @@ This guide explains how cancellation works internally. Understanding these detai
|
|
|
18
18
|
|
|
19
19
|
When you write `new AbortController()` in a workflow function, Workflow DevKit creates a durable controller backed by two existing primitives: a [hook](/docs/foundations/hooks) and a [stream](/docs/foundations/streaming). This page explains why both are needed and how they work together.
|
|
20
20
|
|
|
21
|
-
## The
|
|
21
|
+
## The problem
|
|
22
22
|
|
|
23
|
-
`AbortController` and `AbortSignal` are inherently stateful
|
|
23
|
+
`AbortController` and `AbortSignal` are inherently stateful: an abort happens once and is permanent. In a durable workflow, this state must:
|
|
24
24
|
|
|
25
|
-
1. **Survive replay
|
|
26
|
-
2. **Propagate in real-time
|
|
25
|
+
1. **Survive replay**: If `abort()` was called, `signal.aborted` must return `true` on every subsequent replay of the workflow.
|
|
26
|
+
2. **Propagate in real-time**: A running step on a different compute instance must receive the abort immediately, not on the next replay.
|
|
27
27
|
|
|
28
28
|
No single primitive solves both. Hooks provide durable event log state but can't reach into a running step. Streams provide real-time cross-process communication but aren't part of the event log. The solution is to use both.
|
|
29
29
|
|
|
30
|
-
## Dual
|
|
30
|
+
## Dual backing: hook + stream
|
|
31
31
|
|
|
32
32
|
Every `AbortController` in the workflow context is backed by:
|
|
33
33
|
|
|
34
|
-
### Hook (
|
|
34
|
+
### Hook (durable state)
|
|
35
35
|
|
|
36
|
-
When `new AbortController()` is called in a workflow, an internal hook is created
|
|
36
|
+
When `new AbortController()` is called in a workflow, an internal hook is created, similar to calling `createHook()`. This hook is registered in the workflow's invocations queue and produces events in the [event log](/docs/how-it-works/event-sourcing):
|
|
37
37
|
|
|
38
38
|
- **On creation**: A `hook_created` event records that the controller exists
|
|
39
39
|
- **On abort**: The hook is resumed (producing a `hook_received` event), recording the abort permanently
|
|
40
40
|
- **On replay**: The event consumer processes the `hook_received` event and updates `signal.aborted` to `true` at the same point in the replay as the original abort
|
|
41
41
|
|
|
42
|
-
This gives the workflow deterministic access to the abort state
|
|
42
|
+
This gives the workflow deterministic access to the abort state: `controller.signal.aborted` always returns the correct value, even after cold starts.
|
|
43
43
|
|
|
44
|
-
### Stream (
|
|
44
|
+
### Stream (real-time propagation)
|
|
45
45
|
|
|
46
46
|
When `controller.signal` is serialized as a step argument, a stream name is included in the serialized form. Inside the step, the deserialized `AbortSignal` listens on this stream:
|
|
47
47
|
|
|
@@ -50,7 +50,7 @@ When `controller.signal` is serialized as a step argument, a stream name is incl
|
|
|
50
50
|
|
|
51
51
|
This gives steps real-time cancellation without waiting for the workflow to replay.
|
|
52
52
|
|
|
53
|
-
### Why
|
|
53
|
+
### Why both?
|
|
54
54
|
|
|
55
55
|
| Mechanism | Solves | Doesn't Solve |
|
|
56
56
|
|---|---|---|
|
|
@@ -60,18 +60,18 @@ This gives steps real-time cancellation without waiting for the workflow to repl
|
|
|
60
60
|
|
|
61
61
|
## Lifecycle
|
|
62
62
|
|
|
63
|
-
### 1. Controller
|
|
63
|
+
### 1. Controller created in workflow
|
|
64
64
|
|
|
65
|
-
```
|
|
65
|
+
```text
|
|
66
66
|
new AbortController()
|
|
67
67
|
│
|
|
68
68
|
├─→ Internal hook created (registered in invocations queue)
|
|
69
69
|
└─→ Stream name generated (deterministic ULID)
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
### 2. Signal
|
|
72
|
+
### 2. Signal passed to step
|
|
73
73
|
|
|
74
|
-
```
|
|
74
|
+
```text
|
|
75
75
|
stepFunction(controller.signal)
|
|
76
76
|
│
|
|
77
77
|
├─→ Signal serialized as { streamName, hookToken, aborted }
|
|
@@ -80,9 +80,9 @@ stepFunction(controller.signal)
|
|
|
80
80
|
└─→ Background reader listens on stream for abort packet
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
### 3. abort()
|
|
83
|
+
### 3. abort() called in workflow
|
|
84
84
|
|
|
85
|
-
```
|
|
85
|
+
```text
|
|
86
86
|
controller.abort()
|
|
87
87
|
│
|
|
88
88
|
├─→ signal.aborted set to true (synchronous, local state)
|
|
@@ -92,13 +92,13 @@ controller.abort()
|
|
|
92
92
|
├─→ Suspension handler creates hook_received event
|
|
93
93
|
├─→ Suspension handler writes cancellation packet to stream
|
|
94
94
|
│ │
|
|
95
|
-
│ └─→ Step receives packet → local signal fires → fetch
|
|
95
|
+
│ └─→ Step receives packet → local signal fires → fetch canceled
|
|
96
96
|
└─→ Workflow re-enqueued for replay
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
### 4. Workflow
|
|
99
|
+
### 4. Workflow replays after abort
|
|
100
100
|
|
|
101
|
-
```
|
|
101
|
+
```text
|
|
102
102
|
Replay starts → events loaded
|
|
103
103
|
│
|
|
104
104
|
├─→ new AbortController() → hook created → event consumer subscribes
|
|
@@ -107,34 +107,34 @@ Replay starts → events loaded
|
|
|
107
107
|
└─→ Workflow code sees signal.aborted === true at the correct point in replay
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
On replay, the events consumer re-applies the abort by calling `_setAborted` when it encounters the `hook_received` event in the log
|
|
110
|
+
On replay, the events consumer re-applies the abort by calling `_setAborted` when it encounters the `hook_received` event in the log, at the same point in execution where the original `abort()` happened. This is what makes the abort deterministic across replays.
|
|
111
111
|
|
|
112
|
-
## Where the
|
|
112
|
+
## Where the hook is created
|
|
113
113
|
|
|
114
114
|
The backing hook is set up whenever an `AbortController` or `AbortSignal` enters the workflow context:
|
|
115
115
|
|
|
116
|
-
**`new AbortController()` in a workflow function
|
|
116
|
+
**`new AbortController()` in a workflow function**: The workflow VM provides a durable `AbortController` implementation (similar to how it provides deterministic `Date` and serializable `Request`/`Response`). The hook is created in the constructor using the orchestrator context injected via VM globals.
|
|
117
117
|
|
|
118
|
-
**Returned from a step
|
|
118
|
+
**Returned from a step**: A step can create a plain `new AbortController()` and return it. The step-side serializer generates a stream name and hook token (using a random ULID) and includes them in the serialized payload. When the return value is deserialized into the workflow via `hydrateStepReturnValue`, the workflow reviver reads the token from the payload and sets up the hook with that token. Since the serialized payload is stored in the event log (as part of the `step_completed` event), the same token is used on every replay, so no deterministic generation is needed in the workflow.
|
|
119
119
|
|
|
120
|
-
**Passed as workflow input
|
|
120
|
+
**Passed as workflow input**: Conceptually the same as "returned from a step". The **external reducer** handles it at serialization time:
|
|
121
121
|
|
|
122
122
|
1. Generates a stream name and hook token (random ULID)
|
|
123
123
|
2. Attaches an `abort` event listener on the source signal: when the external code calls `controller.abort()`, the listener writes the cancellation packet to the stream
|
|
124
124
|
3. Pushes the listener's async work into `ops` (awaited via `waitUntil`)
|
|
125
125
|
4. Serializes the reference as `{ streamName, hookToken, aborted }`
|
|
126
126
|
|
|
127
|
-
The serialized payload (including the generated token) is stored in the event log as part of the workflow's input. When the workflow deserializes the input, the reviver reads the token from the payload and creates the hook
|
|
127
|
+
The serialized payload (including the generated token) is stored in the event log as part of the workflow's input. When the workflow deserializes the input, the reviver reads the token from the payload and creates the hook, identical to the "returned from a step" case. On replay, the same token is read from the event log, so the hook matches the same events.
|
|
128
128
|
|
|
129
129
|
If the external code calls `abort()` while the process is still alive (within the `waitUntil` window), the stream packet arrives in the workflow, and the workflow can resume the hook to record it in the event log.
|
|
130
130
|
|
|
131
131
|
<Callout type="info">
|
|
132
|
-
Since the external `AbortController` is a plain JavaScript object (not the workflow VM's durable version), the stream write depends on the originating process still being alive. This is the same constraint that applies to passing a `ReadableStream` as a workflow argument
|
|
132
|
+
Since the external `AbortController` is a plain JavaScript object (not the workflow VM's durable version), the stream write depends on the originating process still being alive. This is the same constraint that applies to passing a `ReadableStream` as a workflow argument: the stream pipe runs via `waitUntil` and requires the process to remain active until the data is written.
|
|
133
133
|
</Callout>
|
|
134
134
|
|
|
135
|
-
## Serialization &
|
|
135
|
+
## Serialization & deserialization
|
|
136
136
|
|
|
137
|
-
### Serialized
|
|
137
|
+
### Serialized form
|
|
138
138
|
|
|
139
139
|
An `AbortController` or `AbortSignal` is serialized as:
|
|
140
140
|
|
|
@@ -148,23 +148,23 @@ An `AbortController` or `AbortSignal` is serialized as:
|
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
The `streamName` and `hookToken` are generated once at serialization time (in the step or external context) and stored in the event log as part of the serialized payload. On replay, the workflow reviver reads them from the payload
|
|
151
|
+
The `streamName` and `hookToken` are generated once at serialization time (in the step or external context) and stored in the event log as part of the serialized payload. On replay, the workflow reviver reads them from the payload; it never generates them itself. This is the same pattern used by `ReadableStream` and `WritableStream` serialization.
|
|
152
152
|
|
|
153
|
-
### Reducers (
|
|
153
|
+
### Reducers (serialization)
|
|
154
154
|
|
|
155
155
|
**In step context** (`getStepReducers`): When a step returns an `AbortController`, the reducer captures the stream name. If `abort()` was called in the step, `aborted: true` is recorded.
|
|
156
156
|
|
|
157
|
-
**In workflow context** (`getWorkflowReducers`): The reducer captures the stream name and hook token. These are handles
|
|
157
|
+
**In workflow context** (`getWorkflowReducers`): The reducer captures the stream name and hook token. These are handles; no I/O happens during serialization in the workflow.
|
|
158
158
|
|
|
159
159
|
**In external context** (`getExternalReducers`): When an `AbortController` is passed as a workflow argument from outside, the reducer creates the backing stream and serializes the reference.
|
|
160
160
|
|
|
161
|
-
### Revivers (
|
|
161
|
+
### Revivers (deserialization)
|
|
162
162
|
|
|
163
163
|
**Into step context** (`getStepRevivers`): Creates a real `AbortController`. If `aborted: true`, calls `abort()` immediately. Otherwise, pushes a stream reader into the step's `ops` array that listens for the cancellation packet and calls `abort()` when received.
|
|
164
164
|
|
|
165
165
|
**Into workflow context** (`getWorkflowRevivers`): Creates the durable AbortController with hook backing. Subscribes to the events consumer for the hook's correlation ID. If the event log contains a `hook_received` event, `signal.aborted` is `true`.
|
|
166
166
|
|
|
167
|
-
### abort() in a
|
|
167
|
+
### abort() in a step
|
|
168
168
|
|
|
169
169
|
When `abort()` is called on a deserialized `AbortController` inside a step:
|
|
170
170
|
|
|
@@ -172,18 +172,18 @@ When `abort()` is called on a deserialized `AbortController` inside a step:
|
|
|
172
172
|
2. The stream write (cancellation packet) is pushed into `ctx.ops`
|
|
173
173
|
3. The hook resume (`resumeHook`) is pushed into `ctx.ops`
|
|
174
174
|
|
|
175
|
-
The step's `ops` array is awaited via `waitUntil(Promise.all(ops))` after the step function returns
|
|
175
|
+
The step's `ops` array is awaited via `waitUntil(Promise.all(ops))` after the step function returns, the same mechanism used by [`getWritable()`](/docs/api-reference/workflow/get-writable). This keeps `abort()` synchronous from the caller's perspective while ensuring the async work completes.
|
|
176
176
|
|
|
177
|
-
### Abort
|
|
177
|
+
### Abort errors are wrapped in FatalError
|
|
178
178
|
|
|
179
|
-
When a step throws due to an abort
|
|
179
|
+
When a step throws due to an abort (whether from `fetch` throwing `AbortError`, `signal.throwIfAborted()`, or any other abort-induced error), the step executor wraps the error in `FatalError` before recording it in the event log. This ensures:
|
|
180
180
|
|
|
181
|
-
- **No retries**: An abort is intentional cancellation, not a transient failure. Retrying would
|
|
181
|
+
- **No retries**: An abort is intentional cancellation, not a transient failure. Retrying would abort again.
|
|
182
182
|
- **Immediate propagation**: The error bubbles up to the workflow as a `FatalError`, which the workflow can catch with `FatalError.is(err)`.
|
|
183
183
|
|
|
184
|
-
The wrapping happens
|
|
184
|
+
The wrapping happens in `runtime/step-executor.ts` during error hydration. When the step's thrown error is an `AbortError` (checked via `err.name === 'AbortError'`), it is treated as fatal regardless of the step's `maxRetries` configuration.
|
|
185
185
|
|
|
186
|
-
### abort() in the
|
|
186
|
+
### abort() in the workflow
|
|
187
187
|
|
|
188
188
|
When `abort()` is called in the workflow context:
|
|
189
189
|
|
|
@@ -194,7 +194,7 @@ When `abort()` is called in the workflow context:
|
|
|
194
194
|
- Creates a `hook_received` event in the event log
|
|
195
195
|
- Writes the cancellation packet to the stream (for real-time step propagation)
|
|
196
196
|
- Re-enqueues the workflow for replay
|
|
197
|
-
|
|
197
|
+
5. On replay, the event consumer processes the `hook_received` event, updating `signal.aborted` to `true` at the deterministically correct point
|
|
198
198
|
|
|
199
199
|
`signal.aborted` is updated synchronously so that the workflow can immediately check the state and serialization captures `aborted: true` when passing the signal to steps. On replay, the event consumer also processes the `hook_received` event, ensuring the state is consistent.
|
|
200
200
|
|
|
@@ -203,55 +203,55 @@ For abort specifically, this ensures that:
|
|
|
203
203
|
- The abort's `hook_received` event is created in the event log
|
|
204
204
|
- The cancellation stream packet is written to propagate to running steps
|
|
205
205
|
|
|
206
|
-
## Race
|
|
206
|
+
## Race conditions
|
|
207
207
|
|
|
208
|
-
### Abort
|
|
208
|
+
### Abort before hook exists
|
|
209
209
|
|
|
210
210
|
When an `AbortSignal` is passed as a workflow argument via `start()`, the external reducer attaches a listener at serialization time. If the external code calls `abort()` before the workflow has started and created the internal hook, the stream packet is written but the hook doesn't exist yet.
|
|
211
211
|
|
|
212
212
|
This is resolved through eventual consistency:
|
|
213
213
|
|
|
214
|
-
1. The stream packet is durable
|
|
214
|
+
1. The stream packet is durable; it persists in storage
|
|
215
215
|
2. When the workflow runs and passes the signal to a step, the step's reviver reads from the stream starting at index 0
|
|
216
216
|
3. The step sees the existing packet, aborts locally, and resumes the hook (via `ops`)
|
|
217
217
|
4. On the next workflow replay, the hook event is in the log and `signal.aborted` is `true`
|
|
218
218
|
|
|
219
|
-
**Important:** There is a window where the workflow's `signal.aborted` returns `false` even though the external code has already called `abort()`. This lasts until a step processes the stream packet and resumes the hook. This is analogous to hooks
|
|
219
|
+
**Important:** There is a window where the workflow's `signal.aborted` returns `false` even though the external code has already called `abort()`. This lasts until a step processes the stream packet and resumes the hook. This is analogous to hooks: `resumeHook()` doesn't take effect until the workflow replays.
|
|
220
220
|
|
|
221
|
-
### Abort at
|
|
221
|
+
### Abort at serialization time
|
|
222
222
|
|
|
223
223
|
To prevent a micro-window where `abort()` is called between checking `signal.aborted` and attaching the listener, the external reducer uses this order:
|
|
224
224
|
|
|
225
225
|
1. Attach the `abort` event listener first
|
|
226
|
-
2. Then check `signal.aborted
|
|
226
|
+
2. Then check `signal.aborted`; if already `true`, the listener won't fire, so handle immediately
|
|
227
227
|
|
|
228
228
|
This ensures no abort events are missed regardless of timing.
|
|
229
229
|
|
|
230
|
-
## Stream/
|
|
230
|
+
## Stream/hook consistency
|
|
231
231
|
|
|
232
232
|
Since abort involves two operations (stream write + hook resume), partial failure is possible:
|
|
233
233
|
|
|
234
|
-
### Stream
|
|
234
|
+
### Stream succeeds, hook fails
|
|
235
235
|
|
|
236
236
|
- Steps see the abort and throw `AbortError` (stream worked)
|
|
237
237
|
- Workflow doesn't see `signal.aborted === true` on the next replay (hook not resumed)
|
|
238
238
|
- The workflow sees the step failure as an error, which it can handle with try/catch
|
|
239
|
-
- **Recovery:** The step-side `resumeHook` call is best-effort
|
|
239
|
+
- **Recovery:** The step-side `resumeHook` call is best-effort: if it throws, the failure is swallowed. Convergence comes from the next replay: when the step's reviver re-reads the stream, it sees the abort packet and calls `resumeHook` again. There's no in-process retry loop; the dual-mechanism design relies on either the stream or the hook eventually landing.
|
|
240
240
|
|
|
241
|
-
### Hook
|
|
241
|
+
### Hook succeeds, stream fails
|
|
242
242
|
|
|
243
243
|
- Workflow sees `signal.aborted === true` on replay (hook worked)
|
|
244
|
-
- Steps don't receive real-time cancellation (stream failed)
|
|
244
|
+
- Steps don't receive real-time cancellation (stream failed), so they run to completion
|
|
245
245
|
- On the next suspension, the workflow knows the abort happened and can stop calling more steps
|
|
246
|
-
- **Recovery:** Natural convergence
|
|
246
|
+
- **Recovery:** Natural convergence. No active harm, only missed real-time cancellation for in-flight steps.
|
|
247
247
|
|
|
248
|
-
### Both
|
|
248
|
+
### Both fail
|
|
249
249
|
|
|
250
|
-
- Abort is lost
|
|
251
|
-
- No crash or corruption
|
|
250
|
+
- Abort is lost; no propagation
|
|
251
|
+
- No crash or corruption; the system continues as if abort was never called
|
|
252
252
|
- **Recovery:** The caller can retry the abort. If using a hook for external cancellation, the hook's retry semantics apply.
|
|
253
253
|
|
|
254
|
-
The dual mechanism provides natural resilience
|
|
254
|
+
The dual mechanism provides natural resilience: if either one succeeds, the system converges on the correct state.
|
|
255
255
|
|
|
256
256
|
## `AbortSignal.timeout()` in Workflow VM
|
|
257
257
|
|
|
@@ -264,7 +264,7 @@ The dual mechanism provides natural resilience — if either one succeeds, the s
|
|
|
264
264
|
A `Request`'s `.signal` is forwarded by the `Request` reducer in two cases:
|
|
265
265
|
|
|
266
266
|
1. **The signal is already aborted.** The serialized payload preserves `aborted: true` and the abort `reason`, so the deserialized step sees the cancellation that happened before the boundary.
|
|
267
|
-
2. **The signal is workflow-managed** (i.e., it has the `ABORT_STREAM_NAME` symbol
|
|
267
|
+
2. **The signal is workflow-managed** (i.e., it has the `ABORT_STREAM_NAME` symbol, produced by a workflow-context `AbortController`). Its hook + stream backing carries through, and the deserialized step listens on the stream as usual.
|
|
268
268
|
|
|
269
269
|
Plain non-aborted native signals are intentionally dropped, including the auto-generated signal that `new Request(url)` synthesizes when no `signal` is passed. Forwarding every Request signal would mint stream infrastructure for the throwaway auto-signals on every Request, even ones the caller never intended to use for cancellation.
|
|
270
270
|
|
|
@@ -278,10 +278,10 @@ await fetchStep(req); // signal carries through
|
|
|
278
278
|
controller.abort(); // step-side fetch sees the abort
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
## Related
|
|
281
|
+
## Related documentation
|
|
282
282
|
|
|
283
|
-
- [Cancellation](/docs/foundations/cancellation)
|
|
284
|
-
- [Event Sourcing](/docs/how-it-works/event-sourcing)
|
|
285
|
-
- [Hooks](/docs/foundations/hooks)
|
|
286
|
-
- [Streaming](/docs/foundations/streaming)
|
|
287
|
-
- [Serialization](/docs/foundations/serialization)
|
|
283
|
+
- [Cancellation](/docs/foundations/cancellation): Usage patterns and API
|
|
284
|
+
- [Event Sourcing](/docs/how-it-works/event-sourcing): How the event log works
|
|
285
|
+
- [Hooks](/docs/foundations/hooks): The hook primitive
|
|
286
|
+
- [Streaming](/docs/foundations/streaming): The stream primitive
|
|
287
|
+
- [Serialization](/docs/foundations/serialization): Serializable types
|