@workflow/core 5.0.0-beta.2 → 5.0.0-beta.21
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/attribute-changes.d.ts +7 -0
- package/dist/attribute-changes.d.ts.map +1 -0
- package/dist/attribute-changes.js +25 -0
- package/dist/capabilities.d.ts +22 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +41 -4
- package/dist/capture-stack.d.ts +16 -0
- package/dist/capture-stack.d.ts.map +1 -0
- package/dist/capture-stack.js +21 -0
- 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 +11 -3
- package/dist/classify-error.d.ts.map +1 -1
- package/dist/classify-error.js +59 -6
- package/dist/context-errors.d.ts +27 -0
- package/dist/context-errors.d.ts.map +1 -0
- package/dist/context-errors.js +101 -0
- package/dist/context-violation-error.d.ts +97 -0
- package/dist/context-violation-error.d.ts.map +1 -0
- package/dist/context-violation-error.js +147 -0
- package/dist/create-hook.d.ts +40 -1
- package/dist/create-hook.d.ts.map +1 -1
- package/dist/create-hook.js +4 -3
- package/dist/define-hook.d.ts.map +1 -1
- package/dist/define-hook.js +20 -5
- package/dist/describe-error.d.ts +70 -0
- package/dist/describe-error.d.ts.map +1 -0
- package/dist/describe-error.js +165 -0
- package/dist/encryption.d.ts +22 -1
- package/dist/encryption.d.ts.map +1 -1
- package/dist/encryption.js +72 -27
- package/dist/events-consumer.d.ts +24 -0
- package/dist/events-consumer.d.ts.map +1 -1
- package/dist/events-consumer.js +79 -16
- package/dist/flushable-stream.d.ts +10 -10
- package/dist/flushable-stream.d.ts.map +1 -1
- package/dist/flushable-stream.js +18 -13
- package/dist/global.d.ts +15 -1
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +21 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/log-format.d.ts +25 -0
- package/dist/log-format.d.ts.map +1 -0
- package/dist/log-format.js +250 -0
- package/dist/logger.d.ts +28 -30
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +77 -31
- package/dist/private.d.ts +107 -2
- package/dist/private.d.ts.map +1 -1
- package/dist/private.js +80 -3
- package/dist/runtime/constants.d.ts +121 -0
- package/dist/runtime/constants.d.ts.map +1 -1
- package/dist/runtime/constants.js +206 -8
- package/dist/runtime/get-port-lazy.d.ts +25 -0
- package/dist/runtime/get-port-lazy.d.ts.map +1 -0
- package/dist/runtime/get-port-lazy.js +92 -0
- package/dist/runtime/get-world-lazy.d.ts +32 -0
- package/dist/runtime/get-world-lazy.d.ts.map +1 -0
- package/dist/runtime/get-world-lazy.js +56 -0
- package/dist/runtime/helpers.d.ts +50 -7
- package/dist/runtime/helpers.d.ts.map +1 -1
- package/dist/runtime/helpers.js +149 -31
- 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 +39 -23
- package/dist/runtime/run-id-time.d.ts +10 -0
- package/dist/runtime/run-id-time.d.ts.map +1 -0
- package/dist/runtime/run-id-time.js +31 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +44 -9
- package/dist/runtime/runs.d.ts.map +1 -1
- package/dist/runtime/runs.js +4 -1
- package/dist/runtime/start.d.ts +30 -1
- package/dist/runtime/start.d.ts.map +1 -1
- package/dist/runtime/start.js +138 -20
- package/dist/runtime/step-executor.d.ts +108 -0
- package/dist/runtime/step-executor.d.ts.map +1 -0
- package/dist/runtime/step-executor.js +715 -0
- package/dist/runtime/step-handler.d.ts.map +1 -1
- package/dist/runtime/step-handler.js +715 -556
- package/dist/runtime/suspension-handler.d.ts +74 -7
- package/dist/runtime/suspension-handler.d.ts.map +1 -1
- package/dist/runtime/suspension-handler.js +308 -116
- package/dist/runtime/wait-continuation.d.ts +84 -0
- package/dist/runtime/wait-continuation.d.ts.map +1 -0
- package/dist/runtime/wait-continuation.js +92 -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-init.d.ts +75 -0
- package/dist/runtime/world-init.d.ts.map +1 -0
- package/dist/runtime/world-init.js +75 -0
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +47 -18
- package/dist/runtime.d.ts +11 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1395 -319
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +1 -1
- package/dist/serialization/client.d.ts +17 -0
- package/dist/serialization/client.d.ts.map +1 -0
- package/dist/serialization/client.js +48 -0
- package/dist/serialization/codec-devalue.d.ts +14 -0
- package/dist/serialization/codec-devalue.d.ts.map +1 -0
- package/dist/serialization/codec-devalue.js +98 -0
- package/dist/serialization/codec.d.ts +106 -0
- package/dist/serialization/codec.d.ts.map +1 -0
- package/dist/serialization/codec.js +17 -0
- package/dist/serialization/compression.d.ts +104 -0
- package/dist/serialization/compression.d.ts.map +1 -0
- package/dist/serialization/compression.js +260 -0
- package/dist/serialization/encryption.d.ts +33 -0
- package/dist/serialization/encryption.d.ts.map +1 -0
- package/dist/serialization/encryption.js +70 -0
- package/dist/serialization/errors.d.ts +34 -0
- package/dist/serialization/errors.d.ts.map +1 -0
- package/dist/serialization/errors.js +59 -0
- package/dist/serialization/format.d.ts +60 -0
- package/dist/serialization/format.d.ts.map +1 -0
- package/dist/serialization/format.js +97 -0
- package/dist/serialization/index.d.ts +19 -0
- package/dist/serialization/index.d.ts.map +1 -0
- package/dist/serialization/index.js +22 -0
- package/dist/serialization/reducers/class.d.ts +11 -0
- package/dist/serialization/reducers/class.d.ts.map +1 -0
- package/dist/serialization/reducers/class.js +68 -0
- package/dist/serialization/reducers/common.d.ts +16 -0
- package/dist/serialization/reducers/common.d.ts.map +1 -0
- package/dist/serialization/reducers/common.js +417 -0
- package/dist/serialization/reducers/step-function.d.ts +35 -0
- package/dist/serialization/reducers/step-function.d.ts.map +1 -0
- package/dist/serialization/reducers/step-function.js +88 -0
- package/dist/serialization/step.d.ts +17 -0
- package/dist/serialization/step.d.ts.map +1 -0
- package/dist/serialization/step.js +48 -0
- package/dist/serialization/types.d.ts +253 -0
- package/dist/serialization/types.d.ts.map +1 -0
- package/dist/serialization/types.js +25 -0
- package/dist/serialization/workflow.d.ts +29 -0
- package/dist/serialization/workflow.d.ts.map +1 -0
- package/dist/serialization/workflow.js +54 -0
- package/dist/serialization-format.d.ts +15 -0
- package/dist/serialization-format.d.ts.map +1 -1
- package/dist/serialization-format.js +209 -11
- package/dist/serialization.d.ts +313 -214
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +1430 -658
- package/dist/set-attributes.d.ts +13 -0
- package/dist/set-attributes.d.ts.map +1 -0
- package/dist/set-attributes.js +42 -0
- package/dist/sleep.d.ts.map +1 -1
- package/dist/sleep.js +3 -2
- package/dist/source-map.d.ts +5 -0
- package/dist/source-map.d.ts.map +1 -1
- package/dist/source-map.js +102 -10
- package/dist/step/context-storage.d.ts +43 -0
- package/dist/step/context-storage.d.ts.map +1 -1
- package/dist/step/context-storage.js +1 -1
- package/dist/step/get-closure-vars.d.ts.map +1 -1
- package/dist/step/get-closure-vars.js +3 -2
- package/dist/step/get-step-metadata.d.ts.map +1 -1
- package/dist/step/get-step-metadata.js +3 -2
- package/dist/step/get-workflow-metadata.d.ts.map +1 -1
- package/dist/step/get-workflow-metadata.js +3 -2
- package/dist/step/writable-stream.d.ts.map +1 -1
- package/dist/step/writable-stream.js +49 -5
- package/dist/step-hydration-cache.d.ts +148 -0
- package/dist/step-hydration-cache.d.ts.map +1 -0
- package/dist/step-hydration-cache.js +171 -0
- package/dist/step.d.ts.map +1 -1
- package/dist/step.js +115 -21
- package/dist/symbols.d.ts +28 -0
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +29 -1
- package/dist/telemetry/semantic-conventions.d.ts +30 -2
- package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
- package/dist/telemetry/semantic-conventions.js +21 -1
- package/dist/telemetry.d.ts +60 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +82 -4
- 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 +16 -6
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +25 -16
- 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 +3 -2
- package/dist/vm/script-cache.d.ts +28 -0
- package/dist/vm/script-cache.d.ts.map +1 -0
- package/dist/vm/script-cache.js +140 -0
- package/dist/workflow/abort-controller.d.ts +65 -0
- package/dist/workflow/abort-controller.d.ts.map +1 -0
- package/dist/workflow/abort-controller.js +277 -0
- 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 +48 -0
- package/dist/workflow/create-hook.d.ts.map +1 -1
- package/dist/workflow/create-hook.js +23 -2
- package/dist/workflow/define-hook.d.ts +1 -1
- package/dist/workflow/define-hook.d.ts.map +1 -1
- package/dist/workflow/define-hook.js +8 -4
- package/dist/workflow/get-workflow-metadata.d.ts.map +1 -1
- package/dist/workflow/get-workflow-metadata.js +14 -3
- package/dist/workflow/hook.d.ts.map +1 -1
- package/dist/workflow/hook.js +207 -33
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +5 -3
- package/dist/workflow/set-attributes.d.ts +73 -0
- package/dist/workflow/set-attributes.d.ts.map +1 -0
- package/dist/workflow/set-attributes.js +65 -0
- package/dist/workflow/sleep.d.ts.map +1 -1
- package/dist/workflow/sleep.js +45 -8
- package/dist/workflow/world-init-stub.d.ts +15 -0
- package/dist/workflow/world-init-stub.d.ts.map +1 -0
- package/dist/workflow/world-init-stub.js +15 -0
- package/dist/workflow.d.ts +10 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +168 -49
- package/docs/api-reference/create-hook.mdx +38 -0
- package/docs/api-reference/create-webhook.mdx +1 -0
- package/docs/api-reference/experimental-set-attributes.mdx +65 -0
- package/docs/api-reference/fetch.mdx +5 -0
- package/docs/api-reference/index.mdx +3 -0
- package/docs/foundations/cancellation.mdx +459 -0
- package/docs/foundations/errors-and-retries.mdx +7 -3
- package/docs/foundations/hooks.mdx +29 -0
- package/docs/foundations/idempotency.mdx +236 -11
- package/docs/foundations/index.mdx +3 -3
- package/docs/foundations/meta.json +3 -2
- package/docs/foundations/serialization.mdx +78 -42
- package/docs/foundations/starting-workflows.mdx +6 -2
- package/docs/foundations/streaming.mdx +14 -23
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +2 -2
- package/docs/how-it-works/encryption.mdx +2 -2
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- package/docs/how-it-works/meta.json +2 -1
- package/package.json +22 -10
- package/docs/foundations/common-patterns.mdx +0 -265
|
@@ -8,6 +8,7 @@ prerequisites:
|
|
|
8
8
|
related:
|
|
9
9
|
- /docs/api-reference/workflow/define-hook
|
|
10
10
|
- /docs/api-reference/workflow/create-webhook
|
|
11
|
+
- /docs/foundations/idempotency
|
|
11
12
|
---
|
|
12
13
|
|
|
13
14
|
Creates a low-level hook primitive that can be used to resume a workflow run with arbitrary payloads.
|
|
@@ -65,6 +66,8 @@ export default Hook;`}
|
|
|
65
66
|
|
|
66
67
|
The returned `Hook` object also implements `AsyncIterable<T>`, which allows you to iterate over incoming payloads using `for await...of` syntax.
|
|
67
68
|
|
|
69
|
+
Use `hook.getConflict()` to check whether the hook token is already claimed by another active hook, without waiting for hook payload data. Calling `createHook()` on its own does not register the hook — registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the registration, then resolves with `null` once `hook_created` is recorded, or with the conflicting [`Run`](/docs/api-reference/workflow-api/get-run) if another active hook already owns the same token.
|
|
70
|
+
|
|
68
71
|
## Examples
|
|
69
72
|
|
|
70
73
|
### Basic Usage
|
|
@@ -112,6 +115,40 @@ export async function slackBotWorkflow(channelId: string) {
|
|
|
112
115
|
}
|
|
113
116
|
```
|
|
114
117
|
|
|
118
|
+
### Detecting Token Conflicts
|
|
119
|
+
|
|
120
|
+
Use `hook.getConflict()` when the workflow needs to claim a hook token before doing other work, but does not need a payload yet:
|
|
121
|
+
|
|
122
|
+
```typescript lineNumbers
|
|
123
|
+
import { createHook } from "workflow";
|
|
124
|
+
|
|
125
|
+
declare function chargeOrder(orderId: string): Promise<void>; // @setup
|
|
126
|
+
|
|
127
|
+
async function processOrder(orderId: string) {
|
|
128
|
+
"use workflow";
|
|
129
|
+
|
|
130
|
+
using hook = createHook({ // [!code highlight]
|
|
131
|
+
token: `order:${orderId}` // [!code highlight]
|
|
132
|
+
}); // [!code highlight]
|
|
133
|
+
|
|
134
|
+
const conflict = await hook.getConflict(); // [!code highlight]
|
|
135
|
+
if (conflict) { // [!code highlight]
|
|
136
|
+
// Another active workflow run already owns this token.
|
|
137
|
+
return { dedupedTo: conflict.runId };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await chargeOrder(orderId);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Because `createHook()` alone does not suspend the workflow, awaiting `hook.getConflict()` is what actually suspends the run and commits the hook registration. It only waits for registration — to receive payload data from a future `resumeHook()` call, await the hook itself or iterate it with `for await...of`.
|
|
145
|
+
|
|
146
|
+
On a conflict, the resolved value is a `Run` handle for the run that currently owns the token, with durable step-backed accessors. The duplicate run can decide in code how to handle it: return or log `conflict.runId`, inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` and continue in the current run. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies in context.
|
|
147
|
+
|
|
148
|
+
<Callout type="info">
|
|
149
|
+
Custom hook tokens are the recommended way to coordinate active workflow runs. Use a deterministic token from your domain, such as an order ID or conversation ID, create the hook near the beginning of the workflow, and check `await hook.getConflict()` before work that depends on owning the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
150
|
+
</Callout>
|
|
151
|
+
|
|
115
152
|
### Waiting for Multiple Payloads
|
|
116
153
|
|
|
117
154
|
You can also wait for multiple payloads by using the `for await...of` syntax.
|
|
@@ -195,3 +232,4 @@ This is equivalent to manually calling `dispose()` but ensures the hook is alway
|
|
|
195
232
|
- [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper
|
|
196
233
|
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload
|
|
197
234
|
- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Higher-level HTTP webhook abstraction
|
|
235
|
+
- [Idempotency](/docs/foundations/idempotency) - Deduplicate step side effects and workflow starts
|
|
@@ -55,6 +55,7 @@ The returned `Webhook` object has:
|
|
|
55
55
|
|
|
56
56
|
- `url`: The HTTP endpoint URL that external systems can call
|
|
57
57
|
- `token`: The unique token identifying this webhook
|
|
58
|
+
- `getConflict()`: A promise that resolves with the conflicting run if another active hook already owns this token, or `null` once the webhook endpoint has been registered
|
|
58
59
|
- Implements `AsyncIterable<T>` for handling multiple requests, where `T` is `Request` (default) or `RequestWithResponse` (manual mode)
|
|
59
60
|
|
|
60
61
|
When using `createWebhook({ respondWith: 'manual' })`, the resolved request type is `RequestWithResponse`, which extends the standard `Request` interface with a `respondWith(response: Response): Promise<void>` method for sending custom responses back to the caller.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: experimental_setAttributes
|
|
3
|
+
description: Attach string metadata to workflow run for observability.
|
|
4
|
+
type: reference
|
|
5
|
+
summary: Use experimental_setAttributes inside a workflow or step function to set run attributes.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
related:
|
|
9
|
+
- /docs/observability/attributes
|
|
10
|
+
- /docs/api-reference/workflow/fatal-error
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Attaches string metadata to the current workflow run.
|
|
14
|
+
|
|
15
|
+
<Callout>
|
|
16
|
+
This API is experimental and may change before the stable attributes API is released.
|
|
17
|
+
</Callout>
|
|
18
|
+
|
|
19
|
+
```typescript lineNumbers
|
|
20
|
+
import { experimental_setAttributes } from "workflow"
|
|
21
|
+
|
|
22
|
+
export async function orderWorkflow(orderId: string) {
|
|
23
|
+
"use workflow"
|
|
24
|
+
|
|
25
|
+
await experimental_setAttributes({
|
|
26
|
+
phase: "received",
|
|
27
|
+
orderId,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## API Signature
|
|
33
|
+
|
|
34
|
+
### Parameters
|
|
35
|
+
|
|
36
|
+
<TSDoc
|
|
37
|
+
definition={`
|
|
38
|
+
import { experimental_setAttributes } from "workflow";
|
|
39
|
+
export default experimental_setAttributes;`}
|
|
40
|
+
showSections={['parameters']}
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
Call `experimental_setAttributes` from a `"use workflow"` function or a `"use step"` function. Calling it from plain application code is not supported because there is no active workflow run.
|
|
46
|
+
|
|
47
|
+
Attribute values must be strings. Pass `undefined` to remove an attribute:
|
|
48
|
+
|
|
49
|
+
```typescript lineNumbers
|
|
50
|
+
import { experimental_setAttributes } from "workflow"
|
|
51
|
+
|
|
52
|
+
export async function cleanupAttributes() {
|
|
53
|
+
"use workflow"
|
|
54
|
+
|
|
55
|
+
await experimental_setAttributes({ staleKey: undefined })
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with `$` are reserved for framework and library code.
|
|
60
|
+
|
|
61
|
+
Validation errors throw [`FatalError`](/docs/api-reference/workflow/fatal-error) and fail the run before an attribute write is attempted.
|
|
62
|
+
|
|
63
|
+
Calls from both workflow and step bodies append a native `attr_set` event, which the World materializes onto `run.attributes`. Workflow-originated events record a workflow writer; step-originated events record the originating step ID and attempt.
|
|
64
|
+
|
|
65
|
+
Native attributes require spec version 4 or later. Step-body storage errors throw from `experimental_setAttributes`; catch them inside the step if the write should be best-effort. Workflow-body writes are committed when the workflow suspends: transient storage errors are retried with the suspension, while a write the World rejects as invalid — such as exceeding the per-run attribute cap across multiple calls — fails the run with the validation error.
|
|
@@ -7,12 +7,17 @@ prerequisites:
|
|
|
7
7
|
- /docs/foundations/workflows-and-steps
|
|
8
8
|
related:
|
|
9
9
|
- /docs/errors/fetch-in-workflow
|
|
10
|
+
- /docs/foundations/idempotency
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
Makes HTTP requests from within a workflow. This is a special step function that wraps the standard `fetch` API, automatically handling serialization and providing retry semantics.
|
|
13
14
|
|
|
14
15
|
This is useful when you need to call external APIs or services from within your workflow.
|
|
15
16
|
|
|
17
|
+
<Callout type="warn">
|
|
18
|
+
Because workflow `fetch()` has retry semantics, use idempotency keys when the request mutates an external system, such as creating a charge, sending an email, or enqueueing work. See [Idempotency](/docs/foundations/idempotency).
|
|
19
|
+
</Callout>
|
|
20
|
+
|
|
16
21
|
<Callout>
|
|
17
22
|
`fetch` is a *special* type of step function provided and should be called directly inside workflow functions.
|
|
18
23
|
</Callout>
|
|
@@ -47,6 +47,9 @@ Workflow SDK contains the following functions you can use inside your workflow f
|
|
|
47
47
|
<Card href="/docs/api-reference/workflow/get-writable" title="getWritable()">
|
|
48
48
|
Access the current workflow run's default stream.
|
|
49
49
|
</Card>
|
|
50
|
+
<Card href="/docs/api-reference/workflow/experimental-set-attributes" title="experimental_setAttributes()">
|
|
51
|
+
Attach experimental string metadata to the current workflow run.
|
|
52
|
+
</Card>
|
|
50
53
|
</Cards>
|
|
51
54
|
|
|
52
55
|
## Error Classes
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Cancellation
|
|
3
|
+
description: Cancel long-running steps cooperatively using AbortSignal, or cancel entire workflow runs.
|
|
4
|
+
type: conceptual
|
|
5
|
+
summary: Cancel in-flight work with AbortSignal or stop entire workflow runs.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
related:
|
|
9
|
+
- /docs/foundations/hooks
|
|
10
|
+
- /docs/how-it-works/cancellation
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Workflow DevKit supports two cancellation mechanisms: **AbortSignal** for fine-grained, cooperative cancellation of individual operations, and **run cancellation** for stopping an entire workflow. This guide covers both.
|
|
14
|
+
|
|
15
|
+
## AbortSignal
|
|
16
|
+
|
|
17
|
+
`AbortController` and `AbortSignal` work across workflow and step boundaries. Create an `AbortController` with `new AbortController()` in a workflow function, pass its signal to steps, and call `abort()` — using the standard [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) API you already know.
|
|
18
|
+
|
|
19
|
+
```typescript lineNumbers
|
|
20
|
+
import { sleep } from "workflow";
|
|
21
|
+
|
|
22
|
+
export async function cancellableWorkflow() {
|
|
23
|
+
"use workflow";
|
|
24
|
+
|
|
25
|
+
const controller = new AbortController(); // [!code highlight]
|
|
26
|
+
|
|
27
|
+
const result = await Promise.race([
|
|
28
|
+
longRunningStep(controller.signal), // [!code highlight]
|
|
29
|
+
sleep("30s").then(() => "timeout" as const),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
if (result === "timeout") {
|
|
33
|
+
controller.abort(); // [!code highlight]
|
|
34
|
+
return { status: "timed out" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { status: "completed", result };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function longRunningStep(signal: AbortSignal) {
|
|
41
|
+
"use step";
|
|
42
|
+
|
|
43
|
+
const response = await fetch("https://api.example.com/slow-operation", {
|
|
44
|
+
signal, // [!code highlight]
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return response.json();
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No special imports, no wrapper functions — just the standard `AbortController` API.
|
|
52
|
+
|
|
53
|
+
<Callout type="info">
|
|
54
|
+
Cancellation is **cooperative**. Aborting a signal doesn't forcefully kill a step — it's up to the step's code to check `signal.aborted` or pass the signal to APIs like `fetch` that respect it. If a step ignores the signal, it runs to completion.
|
|
55
|
+
</Callout>
|
|
56
|
+
|
|
57
|
+
<Callout type="info">
|
|
58
|
+
To learn how `AbortController` works durably across workflow suspensions, replays, and step boundaries, see [How Cancellation Works](/docs/how-it-works/cancellation).
|
|
59
|
+
</Callout>
|
|
60
|
+
|
|
61
|
+
### Timeout with Cancellation
|
|
62
|
+
|
|
63
|
+
Race a step against a timeout, and cancel the step if the timeout wins:
|
|
64
|
+
|
|
65
|
+
```typescript lineNumbers
|
|
66
|
+
import { sleep } from "workflow";
|
|
67
|
+
|
|
68
|
+
export async function fetchWithTimeout(url: string) {
|
|
69
|
+
"use workflow";
|
|
70
|
+
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
|
|
73
|
+
const result = await Promise.race([
|
|
74
|
+
fetchUrl(url, controller.signal),
|
|
75
|
+
sleep("10s").then(() => null),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
if (result === null) {
|
|
79
|
+
controller.abort(); // [!code highlight]
|
|
80
|
+
throw new Error(`Request to ${url} timed out after 10s`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fetchUrl(url: string, signal: AbortSignal) {
|
|
87
|
+
"use step";
|
|
88
|
+
const response = await fetch(url, { signal });
|
|
89
|
+
return response.json();
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Cancelling Parallel Work
|
|
94
|
+
|
|
95
|
+
When racing multiple steps, cancel the losers:
|
|
96
|
+
|
|
97
|
+
```typescript lineNumbers
|
|
98
|
+
export async function firstResponder(urls: string[]) {
|
|
99
|
+
"use workflow";
|
|
100
|
+
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
|
|
103
|
+
const result = await Promise.race( // [!code highlight]
|
|
104
|
+
urls.map((url) => fetchUrl(url, controller.signal)) // [!code highlight]
|
|
105
|
+
); // [!code highlight]
|
|
106
|
+
|
|
107
|
+
controller.abort(); // Cancel remaining fetches // [!code highlight]
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function fetchUrl(url: string, signal: AbortSignal) {
|
|
113
|
+
"use step";
|
|
114
|
+
const response = await fetch(url, { signal });
|
|
115
|
+
return { url, data: await response.json() };
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Passing Signal Through a Pipeline
|
|
120
|
+
|
|
121
|
+
Pass the same signal to a chain of steps. Aborting cancels whichever step is currently running:
|
|
122
|
+
|
|
123
|
+
```typescript lineNumbers
|
|
124
|
+
declare function splitIntoChunks(data: ArrayBuffer): ArrayBuffer[]; // @setup
|
|
125
|
+
declare function processChunk(chunk: ArrayBuffer): Promise<Uint8Array>; // @setup
|
|
126
|
+
|
|
127
|
+
export async function pipelineWorkflow(dataUrl: string) {
|
|
128
|
+
"use workflow";
|
|
129
|
+
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const raw = await downloadData(dataUrl, controller.signal);
|
|
134
|
+
const transformed = await transformData(raw, controller.signal);
|
|
135
|
+
const result = await uploadData(transformed, controller.signal);
|
|
136
|
+
return result;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
139
|
+
return { status: "cancelled" };
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function downloadData(url: string, signal: AbortSignal) {
|
|
146
|
+
"use step";
|
|
147
|
+
const response = await fetch(url, { signal });
|
|
148
|
+
return response.arrayBuffer();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function transformData(data: ArrayBuffer, signal: AbortSignal) {
|
|
152
|
+
"use step";
|
|
153
|
+
|
|
154
|
+
signal.throwIfAborted(); // [!code highlight]
|
|
155
|
+
|
|
156
|
+
const chunks = splitIntoChunks(data);
|
|
157
|
+
const results = [];
|
|
158
|
+
|
|
159
|
+
for (const chunk of chunks) {
|
|
160
|
+
signal.throwIfAborted(); // [!code highlight]
|
|
161
|
+
results.push(await processChunk(chunk));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return Buffer.concat(results);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function uploadData(data: ArrayBuffer, signal: AbortSignal) {
|
|
168
|
+
"use step";
|
|
169
|
+
await fetch("https://storage.example.com/upload", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
body: data,
|
|
172
|
+
signal,
|
|
173
|
+
});
|
|
174
|
+
return { status: "uploaded" };
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Step-Initiated Abort
|
|
179
|
+
|
|
180
|
+
A step can receive the full `AbortController` and call `abort()` to cancel parallel work. This is useful for watchdog/monitor patterns where one step observes an external condition and cancels other in-flight steps:
|
|
181
|
+
|
|
182
|
+
```typescript lineNumbers
|
|
183
|
+
export async function processWithQuotaCheck(userId: string, dataUrl: string) {
|
|
184
|
+
"use workflow";
|
|
185
|
+
|
|
186
|
+
const controller = new AbortController();
|
|
187
|
+
|
|
188
|
+
// Run the work and a quota monitor in parallel
|
|
189
|
+
const [result] = await Promise.all([ // [!code highlight]
|
|
190
|
+
processData(dataUrl, controller.signal), // [!code highlight]
|
|
191
|
+
monitorQuota(userId, controller), // [!code highlight]
|
|
192
|
+
]); // [!code highlight]
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function processData(url: string, signal: AbortSignal) {
|
|
198
|
+
"use step";
|
|
199
|
+
const response = await fetch(url, { signal });
|
|
200
|
+
const data = await response.arrayBuffer();
|
|
201
|
+
// ... expensive processing ...
|
|
202
|
+
return { processed: true };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function monitorQuota(userId: string, controller: AbortController) {
|
|
206
|
+
"use step";
|
|
207
|
+
|
|
208
|
+
// Poll quota status while the other step is running
|
|
209
|
+
while (!controller.signal.aborted) {
|
|
210
|
+
const quota = await fetch(`https://api.example.com/quota/${userId}`);
|
|
211
|
+
const { exceeded } = await quota.json();
|
|
212
|
+
|
|
213
|
+
if (exceeded) {
|
|
214
|
+
controller.abort("Quota exceeded"); // Cancels processData // [!code highlight]
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### User-Triggered Cancellation with Hooks
|
|
224
|
+
|
|
225
|
+
Combine hooks with abort controllers to let users cancel in-flight work from an external API:
|
|
226
|
+
|
|
227
|
+
```typescript lineNumbers
|
|
228
|
+
import { createHook } from "workflow";
|
|
229
|
+
|
|
230
|
+
export async function userCancellableWorkflow(jobId: string) {
|
|
231
|
+
"use workflow";
|
|
232
|
+
|
|
233
|
+
using cancelHook = createHook<{ reason: string }>({
|
|
234
|
+
token: `cancel:${jobId}`,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const workPromise = doExpensiveWork(controller.signal);
|
|
239
|
+
|
|
240
|
+
const result = await Promise.race([ // [!code highlight]
|
|
241
|
+
workPromise.then((data) => ({ status: "completed", data })),
|
|
242
|
+
cancelHook.then((payload) => { // [!code highlight]
|
|
243
|
+
controller.abort(); // [!code highlight]
|
|
244
|
+
return { status: "cancelled", reason: payload.reason };
|
|
245
|
+
}),
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function doExpensiveWork(signal: AbortSignal) {
|
|
252
|
+
"use step";
|
|
253
|
+
const response = await fetch("https://api.example.com/expensive", { signal });
|
|
254
|
+
return response.json();
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```typescript title="app/api/cancel/route.ts" lineNumbers
|
|
259
|
+
import { resumeHook } from "workflow/api";
|
|
260
|
+
|
|
261
|
+
export async function POST(request: Request) {
|
|
262
|
+
const { jobId, reason } = await request.json();
|
|
263
|
+
|
|
264
|
+
await resumeHook(`cancel:${jobId}`, { reason });
|
|
265
|
+
return Response.json({ cancelled: true });
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### How Steps Handle Abort
|
|
270
|
+
|
|
271
|
+
When an `AbortSignal` is aborted, the behavior depends on how the step uses it:
|
|
272
|
+
|
|
273
|
+
| Usage | Behavior on Abort |
|
|
274
|
+
|-------|-------------------|
|
|
275
|
+
| `fetch(url, { signal })` | Request is cancelled, throws `AbortError` |
|
|
276
|
+
| `signal.throwIfAborted()` | Throws the abort reason |
|
|
277
|
+
| `signal.aborted` check | Returns `true`, step can exit gracefully |
|
|
278
|
+
| `signal.addEventListener('abort', fn)` | Callback fires, step can clean up |
|
|
279
|
+
| Ignored | Step runs to completion (abort is cooperative) |
|
|
280
|
+
|
|
281
|
+
### Abort Errors Skip Retries
|
|
282
|
+
|
|
283
|
+
When a step throws due to an abort (e.g., `fetch` throws `AbortError`, or `signal.throwIfAborted()` throws), the error is automatically wrapped in a `FatalError`. This means the step **skips retries** and the error bubbles up to the workflow immediately.
|
|
284
|
+
|
|
285
|
+
This is the correct behavior because an abort is an intentional cancellation — retrying the step would just result in another abort. You don't need to manually wrap abort errors in `FatalError`.
|
|
286
|
+
|
|
287
|
+
```typescript lineNumbers
|
|
288
|
+
import { sleep } from "workflow";
|
|
289
|
+
|
|
290
|
+
export async function workflow() {
|
|
291
|
+
"use workflow";
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const result = await Promise.race([
|
|
296
|
+
cancellableStep(controller.signal),
|
|
297
|
+
sleep("5s").then(() => null),
|
|
298
|
+
]);
|
|
299
|
+
if (result === null) controller.abort();
|
|
300
|
+
return result;
|
|
301
|
+
} catch (err) {
|
|
302
|
+
// AbortError arrives as FatalError — no retries attempted // [!code highlight]
|
|
303
|
+
return { status: "cancelled" };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function cancellableStep(signal: AbortSignal) {
|
|
308
|
+
"use step";
|
|
309
|
+
// If this throws AbortError, it's automatically wrapped in FatalError
|
|
310
|
+
const response = await fetch("https://api.example.com/slow", { signal });
|
|
311
|
+
return response.json();
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Passing AbortSignal as Workflow Input
|
|
316
|
+
|
|
317
|
+
You can pass an `AbortSignal` from external code into a workflow via `start()`:
|
|
318
|
+
|
|
319
|
+
{/* @skip-typecheck: myWorkflow is not declared, this is a conceptual snippet */}
|
|
320
|
+
```typescript lineNumbers
|
|
321
|
+
import { start } from "workflow/api";
|
|
322
|
+
|
|
323
|
+
export async function POST(request: Request) {
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const run = await start(myWorkflow, [controller.signal]); // [!code highlight]
|
|
326
|
+
|
|
327
|
+
// Later, cancel from external code
|
|
328
|
+
controller.abort(); // [!code highlight]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
When the signal is serialized at the `start()` boundary, an event listener is attached to the external signal that writes the cancellation packet to the backing stream. This means the external `abort()` propagates into the workflow — but only while the originating process is still alive (same constraint as passing a `ReadableStream` as input).
|
|
333
|
+
|
|
334
|
+
<Callout type="info">
|
|
335
|
+
For reliable external cancellation that works regardless of process lifetime, prefer the [User-Triggered Cancellation with Hooks](#user-triggered-cancellation-with-hooks) pattern. Hooks are durable and don't depend on the caller's process staying alive.
|
|
336
|
+
</Callout>
|
|
337
|
+
|
|
338
|
+
## Run Cancellation
|
|
339
|
+
|
|
340
|
+
Run cancellation stops an entire workflow at the next suspension point. Unlike `AbortSignal`, it is not cooperative — the workflow does not continue executing after cancellation.
|
|
341
|
+
|
|
342
|
+
```typescript title="app/api/cancel-run/route.ts" lineNumbers
|
|
343
|
+
import { getRun } from "workflow/api";
|
|
344
|
+
|
|
345
|
+
export async function POST(request: Request) {
|
|
346
|
+
const { runId } = await request.json();
|
|
347
|
+
|
|
348
|
+
const run = getRun(runId);
|
|
349
|
+
await run.cancel(); // [!code highlight]
|
|
350
|
+
|
|
351
|
+
return Response.json({ cancelled: true });
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
<Callout type="info">
|
|
356
|
+
Calling `run.cancel()` is the same action as clicking the **Cancel** button on a run in the observability UI — both produce identical `run_cancelled` events in the event log.
|
|
357
|
+
</Callout>
|
|
358
|
+
|
|
359
|
+
When a run is cancelled:
|
|
360
|
+
- The workflow stops at its next suspension point (step call, hook await, or sleep)
|
|
361
|
+
- A `run_cancelled` event is recorded in the [event log](/docs/how-it-works/event-sourcing)
|
|
362
|
+
- All associated hooks are disposed and their tokens released
|
|
363
|
+
- Streams are closed
|
|
364
|
+
|
|
365
|
+
<Callout type="info">
|
|
366
|
+
Run cancellation does **not** automatically abort any outstanding `AbortSignal`s. Steps that are currently executing will run to completion. If you need in-flight cancellation of specific operations, use `AbortSignal`.
|
|
367
|
+
</Callout>
|
|
368
|
+
|
|
369
|
+
## AbortSignal vs. Run Cancellation
|
|
370
|
+
|
|
371
|
+
| | AbortSignal | Run Cancellation |
|
|
372
|
+
|---|---|---|
|
|
373
|
+
| **Scope** | Individual operations within a step | Entire workflow run |
|
|
374
|
+
| **Triggered by** | Your code (`controller.abort()`) | External API (`run.cancel()`) |
|
|
375
|
+
| **Cooperative** | Yes — steps must check the signal | No — workflow stops at the next suspension point |
|
|
376
|
+
| **Granularity** | Can target specific steps or operations | All-or-nothing |
|
|
377
|
+
| **In-flight steps** | Aborted immediately if using the signal | Run to completion |
|
|
378
|
+
|
|
379
|
+
Use `AbortSignal` when you need fine-grained, in-flight cancellation of specific operations. Use run cancellation when you want to stop the entire workflow.
|
|
380
|
+
|
|
381
|
+
## Best Practices
|
|
382
|
+
|
|
383
|
+
**Use `throwIfAborted()` before expensive work.** This throws the signal's abort reason if the signal is already aborted, preventing wasted compute:
|
|
384
|
+
|
|
385
|
+
```typescript lineNumbers
|
|
386
|
+
async function expensiveStep(signal: AbortSignal) {
|
|
387
|
+
"use step";
|
|
388
|
+
signal.throwIfAborted(); // [!code highlight]
|
|
389
|
+
// ... expensive work ...
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Handle abort errors in the workflow.** Abort errors arrive as `FatalError` (no retries) and can be caught with a standard try/catch:
|
|
394
|
+
|
|
395
|
+
```typescript lineNumbers
|
|
396
|
+
declare function cancellableStep(signal: AbortSignal): Promise<void>; // @setup
|
|
397
|
+
import { FatalError } from "workflow";
|
|
398
|
+
|
|
399
|
+
export async function workflow() {
|
|
400
|
+
"use workflow";
|
|
401
|
+
const controller = new AbortController();
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
await cancellableStep(controller.signal);
|
|
405
|
+
} catch (err) {
|
|
406
|
+
if (FatalError.is(err)) { // [!code highlight]
|
|
407
|
+
return { status: "cancelled" };
|
|
408
|
+
}
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Use `AbortSignal.any()` to combine signals:**
|
|
415
|
+
|
|
416
|
+
```typescript lineNumbers
|
|
417
|
+
async function stepWithMultipleSignals(
|
|
418
|
+
userSignal: AbortSignal,
|
|
419
|
+
timeoutSignal: AbortSignal
|
|
420
|
+
) {
|
|
421
|
+
"use step";
|
|
422
|
+
|
|
423
|
+
const combined = AbortSignal.any([userSignal, timeoutSignal]); // [!code highlight]
|
|
424
|
+
const response = await fetch("https://api.example.com/data", {
|
|
425
|
+
signal: combined,
|
|
426
|
+
});
|
|
427
|
+
return response.json();
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**Abort after a race:**
|
|
432
|
+
|
|
433
|
+
```typescript lineNumbers
|
|
434
|
+
declare function stepA(signal: AbortSignal): Promise<string>; // @setup
|
|
435
|
+
declare function stepB(signal: AbortSignal): Promise<string>; // @setup
|
|
436
|
+
|
|
437
|
+
export async function workflow() {
|
|
438
|
+
"use workflow";
|
|
439
|
+
const controller = new AbortController();
|
|
440
|
+
|
|
441
|
+
const winner = await Promise.race([
|
|
442
|
+
stepA(controller.signal),
|
|
443
|
+
stepB(controller.signal),
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
controller.abort(); // Clean up whichever step is still running // [!code highlight]
|
|
447
|
+
return winner;
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
This is safe even if both steps have already completed — aborting a finished operation is a no-op.
|
|
452
|
+
|
|
453
|
+
## Related Documentation
|
|
454
|
+
|
|
455
|
+
- [How Cancellation Works](/docs/how-it-works/cancellation) — Hook and stream backing, serialization internals
|
|
456
|
+
- [Serialization](/docs/foundations/serialization) — Understanding serializable types
|
|
457
|
+
- [Cookbook](/v5/cookbook) — Timeout, race, and other reliability patterns
|
|
458
|
+
- [Hooks](/docs/foundations/hooks) — Pausing workflows for external events
|
|
459
|
+
- [Errors and Retries](/docs/foundations/errors-and-retries) — Handling step failures
|
|
@@ -141,7 +141,7 @@ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
|
|
|
141
141
|
|
|
142
142
|
## Error Codes
|
|
143
143
|
|
|
144
|
-
When a workflow run fails, the error
|
|
144
|
+
When a workflow run fails, the error includes an `errorCode` that classifies the failure, alongside the original thrown value (preserved as `cause`):
|
|
145
145
|
|
|
146
146
|
```typescript lineNumbers
|
|
147
147
|
import { WorkflowRunFailedError } from "@workflow/errors";
|
|
@@ -153,8 +153,12 @@ try {
|
|
|
153
153
|
const result = await run.returnValue;
|
|
154
154
|
} catch (err) {
|
|
155
155
|
if (WorkflowRunFailedError.is(err)) {
|
|
156
|
-
console.log(err.
|
|
157
|
-
|
|
156
|
+
console.log(err.errorCode); // "USER_ERROR", "RUNTIME_ERROR", or undefined
|
|
157
|
+
// `cause` is the original thrown value, hydrated through the workflow
|
|
158
|
+
// serialization pipeline. It can be any thrown value, so check shape.
|
|
159
|
+
if (err.cause instanceof Error) {
|
|
160
|
+
console.log(err.cause.message); // The error message
|
|
161
|
+
}
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
```
|
|
@@ -85,6 +85,35 @@ The key points:
|
|
|
85
85
|
- You need the hook's `token` to resume it
|
|
86
86
|
- The workflow will resume execution right where it left off
|
|
87
87
|
|
|
88
|
+
### Checking for Token Conflicts
|
|
89
|
+
|
|
90
|
+
Sometimes you need to know that a hook token has been claimed, but you do not want to wait for external data yet. Await `hook.getConflict()` for that:
|
|
91
|
+
|
|
92
|
+
```typescript lineNumbers
|
|
93
|
+
import { createHook } from "workflow";
|
|
94
|
+
|
|
95
|
+
declare function processOrder(orderId: string): Promise<void>; // @setup
|
|
96
|
+
|
|
97
|
+
export async function orderWorkflow(orderId: string) {
|
|
98
|
+
"use workflow";
|
|
99
|
+
|
|
100
|
+
using hook = createHook({
|
|
101
|
+
token: `order:${orderId}`
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const conflict = await hook.getConflict(); // [!code highlight]
|
|
105
|
+
if (conflict) { // [!code highlight]
|
|
106
|
+
// Another active run already owns this token.
|
|
107
|
+
return { dedupedTo: conflict.runId };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The hook token is registered and reserved here.
|
|
111
|
+
await processOrder(orderId);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Calling `createHook()` on its own does not register the hook — registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the hook registration, then resolves with `null` once the hook is registered and ready to receive payloads, or with a `Run` handle for the run that owns the token if another active hook already claimed it (see [`HookConflictError`](/docs/errors/hook-conflict)). For `hook_conflict` events persisted by older worlds that did not record the owning run's ID, `getConflict()` rejects with `HookConflictError` instead of resolving with an incomplete handle. The conflicting run's accessors are durable steps, so the workflow can inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` — see [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies.
|
|
116
|
+
|
|
88
117
|
### Custom Tokens for Deterministic Hooks
|
|
89
118
|
|
|
90
119
|
By default, hooks generate a random token. However, you often want to use a **custom token** that external systems can reconstruct. This is especially useful for long-running workflows where the same workflow instance should handle multiple events.
|