@workflow/core 5.0.0-beta.42 → 5.0.0-beta.44
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/capabilities.d.ts +4 -4
- package/dist/capabilities.js +5 -5
- package/dist/capture-stack.d.ts +1 -1
- package/dist/capture-stack.js +2 -2
- package/dist/class-serialization.d.ts +8 -8
- package/dist/class-serialization.js +9 -9
- package/dist/classify-error.js +5 -5
- 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 +5 -5
- package/dist/create-hook.d.ts +10 -6
- 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.js +8 -8
- package/dist/encryption.d.ts +5 -5
- package/dist/encryption.js +7 -7
- package/dist/events-consumer.d.ts +19 -7
- package/dist/events-consumer.d.ts.map +1 -1
- package/dist/events-consumer.js +33 -12
- package/dist/flushable-stream.d.ts +4 -4
- package/dist/flushable-stream.js +12 -12
- package/dist/global.d.ts +1 -1
- package/dist/global.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/log-format.d.ts +2 -2
- package/dist/log-format.js +5 -5
- package/dist/logger.js +4 -4
- package/dist/private.d.ts +39 -64
- package/dist/private.d.ts.map +1 -1
- package/dist/private.js +294 -48
- package/dist/replay-payload-cache.d.ts +1 -1
- package/dist/replay-payload-cache.js +4 -4
- package/dist/runtime/compute-instance.d.ts +4 -3
- package/dist/runtime/compute-instance.d.ts.map +1 -1
- package/dist/runtime/compute-instance.js +5 -4
- package/dist/runtime/constants.d.ts +46 -19
- package/dist/runtime/constants.d.ts.map +1 -1
- package/dist/runtime/constants.js +76 -38
- package/dist/runtime/count-step-started-events.d.ts +7 -7
- package/dist/runtime/count-step-started-events.js +4 -4
- package/dist/runtime/deployment-guard.d.ts +7 -7
- package/dist/runtime/deployment-guard.js +8 -8
- package/dist/runtime/get-port-lazy.d.ts +2 -2
- package/dist/runtime/get-port-lazy.d.ts.map +1 -1
- package/dist/runtime/get-port-lazy.js +37 -30
- package/dist/runtime/get-world-lazy.d.ts +2 -2
- package/dist/runtime/get-world-lazy.js +3 -3
- package/dist/runtime/helpers.d.ts +18 -18
- package/dist/runtime/helpers.js +27 -27
- package/dist/runtime/quickjs-entrypoint.d.ts +23 -10
- package/dist/runtime/quickjs-entrypoint.d.ts.map +1 -1
- package/dist/runtime/quickjs-entrypoint.js +200 -81
- package/dist/runtime/quickjs-runtime.d.ts +31 -15
- package/dist/runtime/quickjs-runtime.d.ts.map +1 -1
- package/dist/runtime/quickjs-runtime.js +126 -77
- package/dist/runtime/quickjs-serde.d.ts +10 -10
- package/dist/runtime/quickjs-serde.js +20 -20
- package/dist/runtime/replay-budget.d.ts +4 -4
- package/dist/runtime/replay-budget.js +5 -5
- package/dist/runtime/replay-recovery-reporter.d.ts +1 -1
- package/dist/runtime/replay-recovery-reporter.js +2 -2
- package/dist/runtime/resume-hook.d.ts +2 -2
- package/dist/runtime/resume-hook.js +27 -27
- package/dist/runtime/resume-latency.d.ts +25 -25
- package/dist/runtime/resume-latency.d.ts.map +1 -1
- package/dist/runtime/resume-latency.js +4 -4
- package/dist/runtime/run-id-time.d.ts +2 -2
- package/dist/runtime/run-id-time.js +5 -5
- package/dist/runtime/run.d.ts +21 -4
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +154 -35
- package/dist/runtime/runs.d.ts +4 -4
- package/dist/runtime/runs.js +2 -2
- package/dist/runtime/start.d.ts +3 -3
- package/dist/runtime/start.d.ts.map +1 -1
- package/dist/runtime/start.js +27 -23
- package/dist/runtime/step-executor.d.ts +59 -11
- package/dist/runtime/step-executor.d.ts.map +1 -1
- package/dist/runtime/step-executor.js +116 -42
- package/dist/runtime/step-latency.d.ts +20 -14
- package/dist/runtime/step-latency.d.ts.map +1 -1
- package/dist/runtime/step-latency.js +13 -11
- package/dist/runtime/step-ownership.d.ts +6 -6
- package/dist/runtime/step-ownership.js +8 -8
- package/dist/runtime/step-single-flight.d.ts +1 -1
- package/dist/runtime/step-single-flight.d.ts.map +1 -1
- package/dist/runtime/step-single-flight.js +17 -11
- package/dist/runtime/suspension-handler.d.ts +99 -7
- package/dist/runtime/suspension-handler.d.ts.map +1 -1
- package/dist/runtime/suspension-handler.js +678 -57
- 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 +1 -1
- package/dist/runtime/vm-mode.js +2 -2
- package/dist/runtime/wait-continuation.d.ts +31 -9
- package/dist/runtime/wait-continuation.d.ts.map +1 -1
- package/dist/runtime/wait-continuation.js +44 -13
- package/dist/runtime/wait-until.d.ts +1 -1
- package/dist/runtime/wait-until.js +2 -2
- package/dist/runtime/world-compatibility.d.ts +20 -9
- package/dist/runtime/world-compatibility.d.ts.map +1 -1
- package/dist/runtime/world-compatibility.js +24 -13
- package/dist/runtime/world-init.d.ts +5 -5
- package/dist/runtime/world-init.js +6 -6
- package/dist/runtime/world.d.ts.map +1 -1
- package/dist/runtime/world.js +13 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +417 -213
- package/dist/sealed-box.d.ts +7 -7
- package/dist/sealed-box.js +15 -15
- package/dist/serialization/client.js +2 -2
- package/dist/serialization/codec-devalue-vm.d.ts +1 -1
- package/dist/serialization/codec-devalue-vm.js +4 -4
- package/dist/serialization/codec-devalue.d.ts +1 -1
- package/dist/serialization/codec-devalue.js +2 -2
- package/dist/serialization/codec.d.ts +4 -4
- package/dist/serialization/codec.js +2 -2
- package/dist/serialization/compression.d.ts +12 -11
- package/dist/serialization/compression.d.ts.map +1 -1
- package/dist/serialization/compression.js +16 -15
- package/dist/serialization/encryption.d.ts +7 -7
- package/dist/serialization/encryption.d.ts.map +1 -1
- package/dist/serialization/encryption.js +8 -8
- package/dist/serialization/errors.js +2 -2
- package/dist/serialization/format.d.ts +6 -6
- package/dist/serialization/format.js +7 -7
- package/dist/serialization/hardened.d.ts +18 -18
- package/dist/serialization/hardened.d.ts.map +1 -1
- package/dist/serialization/hardened.js +41 -32
- package/dist/serialization/index.d.ts +1 -1
- package/dist/serialization/index.js +2 -2
- package/dist/serialization/reducers/class-vm.d.ts +1 -1
- package/dist/serialization/reducers/class-vm.js +2 -2
- package/dist/serialization/reducers/class.js +2 -2
- package/dist/serialization/reducers/common-vm.js +12 -12
- package/dist/serialization/reducers/common.js +12 -12
- package/dist/serialization/reducers/step-function-vm.d.ts +1 -1
- package/dist/serialization/reducers/step-function-vm.js +2 -2
- package/dist/serialization/reducers/step-function.js +2 -2
- package/dist/serialization/step.js +2 -2
- package/dist/serialization/types.d.ts +5 -5
- package/dist/serialization/types.js +2 -2
- package/dist/serialization/workflow-vm.d.ts +1 -1
- package/dist/serialization/workflow-vm.d.ts.map +1 -1
- package/dist/serialization/workflow-vm.js +13 -10
- package/dist/serialization-format.d.ts +11 -11
- package/dist/serialization-format.d.ts.map +1 -1
- package/dist/serialization-format.js +29 -23
- package/dist/serialization.d.ts +20 -12
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +141 -99
- package/dist/set-attributes.js +2 -2
- package/dist/source-map.d.ts +1 -1
- package/dist/source-map.d.ts.map +1 -1
- package/dist/source-map.js +18 -12
- package/dist/step/context-storage.d.ts +5 -5
- package/dist/step/context-storage.js +1 -1
- package/dist/step/writable-stream.d.ts +3 -3
- package/dist/step/writable-stream.js +7 -7
- package/dist/step.js +12 -12
- package/dist/symbols.d.ts +2 -2
- package/dist/symbols.js +3 -3
- package/dist/telemetry/semantic-conventions.d.ts +21 -13
- package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
- package/dist/telemetry/semantic-conventions.js +21 -15
- package/dist/telemetry.d.ts +9 -3
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +40 -10
- package/dist/test-support/orchestrator-context.d.ts.map +1 -1
- package/dist/test-support/orchestrator-context.js +12 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/dist/vm/index.js +4 -4
- package/dist/vm/script-cache.d.ts.map +1 -1
- package/dist/vm/script-cache.js +23 -17
- package/dist/vm/uint8array-base64.js +1 -1
- package/dist/workflow/abort-controller.d.ts +1 -1
- package/dist/workflow/abort-controller.js +12 -12
- package/dist/workflow/create-hook.js +3 -3
- package/dist/workflow/get-workflow-metadata.js +2 -2
- package/dist/workflow/hook.js +18 -18
- package/dist/workflow/set-attributes.d.ts +1 -1
- package/dist/workflow/set-attributes.js +2 -2
- package/dist/workflow/sleep.js +2 -2
- package/dist/workflow/world-init-stub.d.ts +1 -1
- package/dist/workflow/world-init-stub.js +2 -2
- package/dist/workflow.d.ts +2 -2
- package/dist/workflow.js +36 -14
- package/docs/api-reference/create-hook.mdx +15 -15
- package/docs/api-reference/create-webhook.mdx +15 -15
- package/docs/api-reference/define-hook.mdx +10 -10
- package/docs/api-reference/fatal-error.mdx +2 -2
- package/docs/api-reference/fetch.mdx +7 -7
- 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 +1 -1
- package/docs/api-reference/retryable-error.mdx +1 -1
- package/docs/api-reference/set-attributes.mdx +2 -2
- package/docs/api-reference/sleep.mdx +3 -3
- package/docs/foundations/cancellation.mdx +31 -31
- package/docs/foundations/errors-and-retries.mdx +42 -11
- package/docs/foundations/hooks.mdx +37 -37
- package/docs/foundations/idempotency.mdx +9 -9
- package/docs/foundations/serialization.mdx +21 -22
- package/docs/foundations/starting-workflows.mdx +36 -37
- package/docs/foundations/streaming.mdx +68 -39
- package/docs/foundations/versioning.mdx +3 -3
- package/docs/foundations/workflows-and-steps.mdx +9 -9
- package/docs/how-it-works/cancellation.mdx +62 -62
- package/docs/how-it-works/code-transform.mdx +66 -54
- package/docs/how-it-works/encryption.mdx +25 -21
- package/docs/how-it-works/event-sourcing.mdx +84 -28
- package/docs/how-it-works/framework-integrations.mdx +12 -12
- package/docs/how-it-works/understanding-directives.mdx +21 -21
- package/package.json +7 -7
|
@@ -14,7 +14,7 @@ Workflow DevKit supports two cancellation mechanisms: **AbortSignal** for fine-g
|
|
|
14
14
|
|
|
15
15
|
## AbortSignal
|
|
16
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()`
|
|
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.
|
|
18
18
|
|
|
19
19
|
```typescript lineNumbers
|
|
20
20
|
import { sleep } from "workflow";
|
|
@@ -48,17 +48,17 @@ async function longRunningStep(signal: AbortSignal) {
|
|
|
48
48
|
}
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Use the standard `AbortController` API without special imports or wrapper functions.
|
|
52
52
|
|
|
53
53
|
<Callout type="info">
|
|
54
|
-
Cancellation is **cooperative**. Aborting a signal doesn't forcefully kill a step
|
|
54
|
+
Cancellation is **cooperative**. Aborting a signal doesn't forcefully kill a step. The step's code must 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
55
|
</Callout>
|
|
56
56
|
|
|
57
57
|
<Callout type="info">
|
|
58
58
|
To learn how `AbortController` works durably across workflow suspensions, replays, and step boundaries, see [How Cancellation Works](/docs/how-it-works/cancellation).
|
|
59
59
|
</Callout>
|
|
60
60
|
|
|
61
|
-
### Timeout with
|
|
61
|
+
### Timeout with cancellation
|
|
62
62
|
|
|
63
63
|
Race a step against a timeout, and cancel the step if the timeout wins:
|
|
64
64
|
|
|
@@ -90,7 +90,7 @@ async function fetchUrl(url: string, signal: AbortSignal) {
|
|
|
90
90
|
}
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
### Cancelling
|
|
93
|
+
### Cancelling parallel work
|
|
94
94
|
|
|
95
95
|
When racing multiple steps, cancel the losers:
|
|
96
96
|
|
|
@@ -116,7 +116,7 @@ async function fetchUrl(url: string, signal: AbortSignal) {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
### Passing
|
|
119
|
+
### Passing signal through a pipeline
|
|
120
120
|
|
|
121
121
|
Pass the same signal to a chain of steps. Aborting cancels whichever step is currently running:
|
|
122
122
|
|
|
@@ -175,7 +175,7 @@ async function uploadData(data: ArrayBuffer, signal: AbortSignal) {
|
|
|
175
175
|
}
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
### Step-
|
|
178
|
+
### Step-initiated abort
|
|
179
179
|
|
|
180
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
181
|
|
|
@@ -220,7 +220,7 @@ async function monitorQuota(userId: string, controller: AbortController) {
|
|
|
220
220
|
}
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
### User-
|
|
223
|
+
### User-triggered cancellation with hooks
|
|
224
224
|
|
|
225
225
|
Combine hooks with abort controllers to let users cancel in-flight work from an external API:
|
|
226
226
|
|
|
@@ -266,23 +266,23 @@ export async function POST(request: Request) {
|
|
|
266
266
|
}
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
-
### How
|
|
269
|
+
### How steps handle abort
|
|
270
270
|
|
|
271
271
|
When an `AbortSignal` is aborted, the behavior depends on how the step uses it:
|
|
272
272
|
|
|
273
|
-
| Usage | Behavior on
|
|
273
|
+
| Usage | Behavior on abort |
|
|
274
274
|
|-------|-------------------|
|
|
275
|
-
| `fetch(url, { signal })` | Request is
|
|
275
|
+
| `fetch(url, { signal })` | Request is canceled, throws `AbortError` |
|
|
276
276
|
| `signal.throwIfAborted()` | Throws the abort reason |
|
|
277
277
|
| `signal.aborted` check | Returns `true`, step can exit gracefully |
|
|
278
278
|
| `signal.addEventListener('abort', fn)` | Callback fires, step can clean up |
|
|
279
279
|
| Ignored | Step runs to completion (abort is cooperative) |
|
|
280
280
|
|
|
281
|
-
### Abort
|
|
281
|
+
### Abort errors skip retries
|
|
282
282
|
|
|
283
|
-
When a step throws due to an abort (
|
|
283
|
+
When a step throws due to an abort (for example, `fetch` throws `AbortError`, or `signal.throwIfAborted()` throws), the runtime wraps the error in a `FatalError`. The step **skips retries**, and the error immediately bubbles up to the workflow.
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
An abort is an intentional cancellation, so retrying the step would result in another abort. You don't need to manually wrap abort errors in `FatalError`.
|
|
286
286
|
|
|
287
287
|
```typescript lineNumbers
|
|
288
288
|
import { sleep } from "workflow";
|
|
@@ -299,7 +299,7 @@ export async function workflow() {
|
|
|
299
299
|
if (result === null) controller.abort();
|
|
300
300
|
return result;
|
|
301
301
|
} catch (err) {
|
|
302
|
-
// AbortError arrives as FatalError
|
|
302
|
+
// AbortError arrives as FatalError, with no retries attempted // [!code highlight]
|
|
303
303
|
return { status: "cancelled" };
|
|
304
304
|
}
|
|
305
305
|
}
|
|
@@ -312,7 +312,7 @@ async function cancellableStep(signal: AbortSignal) {
|
|
|
312
312
|
}
|
|
313
313
|
```
|
|
314
314
|
|
|
315
|
-
### Passing AbortSignal as
|
|
315
|
+
### Passing AbortSignal as workflow input
|
|
316
316
|
|
|
317
317
|
You can pass an `AbortSignal` from external code into a workflow via `start()`:
|
|
318
318
|
|
|
@@ -329,15 +329,15 @@ export async function POST(request: Request) {
|
|
|
329
329
|
}
|
|
330
330
|
```
|
|
331
331
|
|
|
332
|
-
When the signal is serialized at the `start()` boundary, an event listener
|
|
332
|
+
When the signal is serialized at the `start()` boundary, an event listener attaches to the external signal and writes the cancellation packet to the backing stream. The external `abort()` propagates into the workflow only while the originating process is still alive (the same constraint as passing a `ReadableStream` as input).
|
|
333
333
|
|
|
334
334
|
<Callout type="info">
|
|
335
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
336
|
</Callout>
|
|
337
337
|
|
|
338
|
-
## Run
|
|
338
|
+
## Run cancellation
|
|
339
339
|
|
|
340
|
-
Run cancellation stops an entire workflow at the next suspension point. Unlike `AbortSignal`, it is not cooperative
|
|
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
341
|
|
|
342
342
|
```typescript title="app/api/cancel-run/route.ts" lineNumbers
|
|
343
343
|
import { getRun } from "workflow/api";
|
|
@@ -353,10 +353,10 @@ export async function POST(request: Request) {
|
|
|
353
353
|
```
|
|
354
354
|
|
|
355
355
|
<Callout type="info">
|
|
356
|
-
Calling `run.cancel()` is the same action as clicking the **Cancel** button on a run in the observability UI
|
|
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
357
|
</Callout>
|
|
358
358
|
|
|
359
|
-
When a run is
|
|
359
|
+
When a run is canceled:
|
|
360
360
|
- The workflow stops at its next suspension point (step call, hook await, or sleep)
|
|
361
361
|
- A `run_cancelled` event is recorded in the [event log](/docs/how-it-works/event-sourcing)
|
|
362
362
|
- All associated hooks are disposed and their tokens released
|
|
@@ -366,19 +366,19 @@ When a run is cancelled:
|
|
|
366
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
367
|
</Callout>
|
|
368
368
|
|
|
369
|
-
## AbortSignal vs.
|
|
369
|
+
## AbortSignal vs. run cancellation
|
|
370
370
|
|
|
371
371
|
| | AbortSignal | Run Cancellation |
|
|
372
372
|
|---|---|---|
|
|
373
373
|
| **Scope** | Individual operations within a step | Entire workflow run |
|
|
374
374
|
| **Triggered by** | Your code (`controller.abort()`) | External API (`run.cancel()`) |
|
|
375
|
-
| **Cooperative** | Yes
|
|
375
|
+
| **Cooperative** | Yes. Steps must check the signal | No. The workflow stops at the next suspension point |
|
|
376
376
|
| **Granularity** | Can target specific steps or operations | All-or-nothing |
|
|
377
377
|
| **In-flight steps** | Aborted immediately if using the signal | Run to completion |
|
|
378
378
|
|
|
379
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
380
|
|
|
381
|
-
## Best
|
|
381
|
+
## Best practices
|
|
382
382
|
|
|
383
383
|
**Use `throwIfAborted()` before expensive work.** This throws the signal's abort reason if the signal is already aborted, preventing wasted compute:
|
|
384
384
|
|
|
@@ -448,12 +448,12 @@ export async function workflow() {
|
|
|
448
448
|
}
|
|
449
449
|
```
|
|
450
450
|
|
|
451
|
-
This is safe even if both steps have already completed
|
|
451
|
+
This is safe even if both steps have already completed because aborting a finished operation is a no-op.
|
|
452
452
|
|
|
453
|
-
## Related
|
|
453
|
+
## Related documentation
|
|
454
454
|
|
|
455
|
-
- [How Cancellation Works](/docs/how-it-works/cancellation)
|
|
456
|
-
- [Serialization](/docs/foundations/serialization)
|
|
457
|
-
- [Cookbook](/
|
|
458
|
-
- [Hooks](/docs/foundations/hooks)
|
|
459
|
-
- [Errors and Retries](/docs/foundations/errors-and-retries)
|
|
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](/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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Errors & Retrying
|
|
3
|
-
description: Customize retry behavior with FatalError and RetryableError
|
|
3
|
+
description: Customize retry behavior with FatalError and RetryableError.
|
|
4
4
|
type: conceptual
|
|
5
5
|
summary: Control how steps handle failures and customize retry behavior.
|
|
6
6
|
prerequisites:
|
|
@@ -12,7 +12,7 @@ related:
|
|
|
12
12
|
|
|
13
13
|
By default, errors thrown inside steps are retried. Additionally, Workflow SDK provides two new types of errors you can use to customize retries.
|
|
14
14
|
|
|
15
|
-
## Default
|
|
15
|
+
## Default retrying
|
|
16
16
|
|
|
17
17
|
By default, steps retry up to 3 times on arbitrary errors. You can customize the number of retries by adding a `maxRetries` property to the step function.
|
|
18
18
|
|
|
@@ -42,9 +42,9 @@ Steps get enqueued immediately after a failure. Read on to see how this can be c
|
|
|
42
42
|
more information.
|
|
43
43
|
</Callout>
|
|
44
44
|
|
|
45
|
-
## Intentional
|
|
45
|
+
## Intentional errors
|
|
46
46
|
|
|
47
|
-
When your step needs to intentionally throw an error and skip retrying,
|
|
47
|
+
When your step needs to intentionally throw an error and skip retrying, throw a [`FatalError`](/docs/api-reference/workflow/fatal-error).
|
|
48
48
|
|
|
49
49
|
```typescript lineNumbers
|
|
50
50
|
import { FatalError } from "workflow";
|
|
@@ -67,7 +67,7 @@ async function callApi(endpoint: string) {
|
|
|
67
67
|
}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
## Customize
|
|
70
|
+
## Customize retry behavior
|
|
71
71
|
|
|
72
72
|
When you need to customize the delay on a retry, use [`RetryableError`](/docs/api-reference/workflow/retryable-error) and set the `retryAfter` property.
|
|
73
73
|
|
|
@@ -97,7 +97,7 @@ async function callApi(endpoint: string) {
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
## Advanced
|
|
100
|
+
## Advanced example
|
|
101
101
|
|
|
102
102
|
This final example combines everything we've learned, along with [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata).
|
|
103
103
|
|
|
@@ -139,7 +139,32 @@ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
|
|
|
139
139
|
step can run up to 4 times total (1 initial attempt + 3 retries).
|
|
140
140
|
</Callout>
|
|
141
141
|
|
|
142
|
-
##
|
|
142
|
+
## Serialization failures
|
|
143
|
+
|
|
144
|
+
A step whose arguments or return value cannot be [serialized](/docs/foundations/serialization) fails like a step whose body threw a `FatalError`: the failure is deterministic, so it skips the retry loop, and a `try/catch` around the step call observes the `SerializationError`:
|
|
145
|
+
|
|
146
|
+
```typescript lineNumbers
|
|
147
|
+
async function someStep(input: unknown) {
|
|
148
|
+
"use step";
|
|
149
|
+
return input;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function myWorkflow(input: unknown) {
|
|
153
|
+
"use workflow";
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await someStep(input);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if ((err as Error).name === "SerializationError") {
|
|
159
|
+
// e.g. `Failed to serialize step arguments at path "..."`
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Uncaught, the run fails immediately with the `USER_ERROR` code, without retrying. See [serialization-failed](/docs/errors/serialization-failed) for common causes and fixes.
|
|
166
|
+
|
|
167
|
+
## Error codes
|
|
143
168
|
|
|
144
169
|
When a workflow run fails, the error includes an `errorCode` that classifies the failure, alongside the original thrown value (preserved as `cause`):
|
|
145
170
|
|
|
@@ -153,7 +178,7 @@ try {
|
|
|
153
178
|
const result = await run.returnValue;
|
|
154
179
|
} catch (err) {
|
|
155
180
|
if (WorkflowRunFailedError.is(err)) {
|
|
156
|
-
console.log(err.errorCode); // "USER_ERROR", "
|
|
181
|
+
console.log(err.errorCode); // e.g. "USER_ERROR", "MAX_EVENTS_EXCEEDED", or undefined
|
|
157
182
|
// `cause` is the original thrown value, hydrated through the workflow
|
|
158
183
|
// serialization pipeline. It can be any thrown value, so check shape.
|
|
159
184
|
if (err.cause instanceof Error) {
|
|
@@ -166,13 +191,19 @@ try {
|
|
|
166
191
|
| Code | Meaning |
|
|
167
192
|
| --- | --- |
|
|
168
193
|
| `USER_ERROR` | An error thrown in your workflow or step code (including propagated step failures like `FatalError`) |
|
|
169
|
-
| `
|
|
194
|
+
| `MAX_EVENTS_EXCEEDED` | The run reached the World's per-run event ceiling (25,000 on the Local and Vercel Worlds). Split unbounded loops into [child workflows](/cookbook/advanced/child-workflows); see [Limits](/docs/configuration/runtime-tuning#limits) |
|
|
195
|
+
| `MAX_DELIVERIES_EXCEEDED` | The run exceeded the maximum number of queue deliveries |
|
|
196
|
+
| `REPLAY_TIMEOUT` | A workflow replay exceeded the maximum allowed duration |
|
|
197
|
+
| `REPLAY_DIVERGENCE` | A replay could not consume the event log deterministically, usually because of non-deterministic workflow code. |
|
|
198
|
+
| `CORRUPTED_EVENT_LOG` | The event log contains orphaned or mismatched events and cannot be replayed. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
|
|
199
|
+
| `WORLD_CONTRACT_ERROR` | A World response violated the SDK contract; points at a World implementation bug |
|
|
200
|
+
| `RUNTIME_ERROR` | An internal runtime error. If you see this, please [file an issue](https://github.com/vercel/workflow/issues) |
|
|
170
201
|
|
|
171
202
|
<Callout type="info">
|
|
172
|
-
The error code is also available on the run entity
|
|
203
|
+
The error code is also available on the run entity through the Workflow CLI (`npx workflow inspect runs <runId>`) in the `error.code` field, and as an OpenTelemetry span attribute (`workflow.error.code`) for observability.
|
|
173
204
|
</Callout>
|
|
174
205
|
|
|
175
|
-
## Rolling
|
|
206
|
+
## Rolling back failed steps
|
|
176
207
|
|
|
177
208
|
When a workflow fails partway through, it can leave the system in an inconsistent state.
|
|
178
209
|
A common pattern to address this is "rollbacks": for each successful step, record a corresponding rollback action that can undo it.
|
|
@@ -11,9 +11,9 @@ related:
|
|
|
11
11
|
- /docs/ai/human-in-the-loop
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
Hooks
|
|
14
|
+
Hooks pause workflow execution and resume it later with external data. Workflows can wait for external events, user interactions (also known as "human in the loop"), or HTTP requests.
|
|
15
15
|
|
|
16
|
-
## Understanding
|
|
16
|
+
## Understanding hooks
|
|
17
17
|
|
|
18
18
|
At their core, **Hooks** are a low-level primitive that allows you to pause a workflow and resume it later with arbitrary [serializable data](/docs/foundations/serialization). Think of them as suspension points in your workflow where you're waiting for external input.
|
|
19
19
|
|
|
@@ -23,9 +23,9 @@ When you create a hook, it generates a unique token that external systems can us
|
|
|
23
23
|
- Receiving data from an external system or service
|
|
24
24
|
- Implementing event-driven workflows that react to multiple events over time
|
|
25
25
|
|
|
26
|
-
### Creating
|
|
26
|
+
### Creating your first hook
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
This workflow creates a hook and waits for external data:
|
|
29
29
|
|
|
30
30
|
```typescript lineNumbers
|
|
31
31
|
import { createHook } from "workflow";
|
|
@@ -59,7 +59,7 @@ We recommend using the `using` keyword which implements the [TC39 Explicit Resou
|
|
|
59
59
|
See the full API reference for [`createHook()`](/docs/api-reference/workflow/create-hook) for all available options.
|
|
60
60
|
</Callout>
|
|
61
61
|
|
|
62
|
-
### Resuming a
|
|
62
|
+
### Resuming a hook
|
|
63
63
|
|
|
64
64
|
To send data to a waiting workflow, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) from an API route, server action, or any other external context:
|
|
65
65
|
|
|
@@ -85,7 +85,7 @@ 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
|
|
88
|
+
### Checking for token conflicts
|
|
89
89
|
|
|
90
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
91
|
|
|
@@ -112,11 +112,11 @@ export async function orderWorkflow(orderId: string) {
|
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
Calling `createHook()` on its own does not register the hook
|
|
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 (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
116
|
|
|
117
|
-
### Custom
|
|
117
|
+
### Custom tokens for deterministic hooks
|
|
118
118
|
|
|
119
|
-
By default, hooks generate
|
|
119
|
+
By default, hooks generate their own 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.
|
|
120
120
|
|
|
121
121
|
For example, imagine a Slack bot where each channel should have its own workflow instance:
|
|
122
122
|
|
|
@@ -168,9 +168,9 @@ export async function POST(request: Request) {
|
|
|
168
168
|
}
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
### Receiving
|
|
171
|
+
### Receiving multiple events
|
|
172
172
|
|
|
173
|
-
Hooks are _reusable_
|
|
173
|
+
Hooks are _reusable_. They implement `AsyncIterable`, which means you can use `for await...of` to receive multiple events over time:
|
|
174
174
|
|
|
175
175
|
```typescript lineNumbers
|
|
176
176
|
import { createHook } from "workflow";
|
|
@@ -198,7 +198,7 @@ export async function dataCollectionWorkflow() {
|
|
|
198
198
|
|
|
199
199
|
Each time you call `resumeHook()` with the same token, the loop receives another value.
|
|
200
200
|
|
|
201
|
-
### Disposing
|
|
201
|
+
### Disposing hooks early
|
|
202
202
|
|
|
203
203
|
When a workflow ends, hooks are automatically disposed. However, you may want to release a hook token early so another workflow can use it while your workflow continues running. Use a block scope with `using` to control when disposal happens:
|
|
204
204
|
|
|
@@ -240,9 +240,9 @@ hook.dispose(); // Manually release the token
|
|
|
240
240
|
After disposal, the hook will no longer receive events and the async iterator will stop yielding values.
|
|
241
241
|
</Callout>
|
|
242
242
|
|
|
243
|
-
## Understanding
|
|
243
|
+
## Understanding webhooks
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
Hooks require you to manually handle HTTP requests and route them to workflows. **Webhooks** provide a higher-level abstraction built on top of hooks that:
|
|
246
246
|
|
|
247
247
|
1. Automatically serializes the entire HTTP [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
|
|
248
248
|
2. Provides an automatically addressable `url` property pointing to the generated webhook endpoint
|
|
@@ -251,16 +251,16 @@ While hooks are powerful, they require you to manually handle HTTP requests and
|
|
|
251
251
|
When using Workflow SDK, webhooks are automatically wired up at `/.well-known/workflow/v1/webhook/:token` without any additional setup.
|
|
252
252
|
|
|
253
253
|
<Callout type="warn">
|
|
254
|
-
`createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes
|
|
254
|
+
`createWebhook()` exposes a public route at `/.well-known/workflow/v1/webhook/:token`, and the token in that URL is the only authorization performed for incoming requests. This is convenient for prototypes because you can share the webhook URL (endpoint) without creating another route, but if you need stronger security, prefer [`createHook()`](/docs/api-reference/workflow/create-hook) behind your own route and authorize the request before calling [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) to avoid unauthenticated workflow resumptions.
|
|
255
255
|
</Callout>
|
|
256
256
|
|
|
257
257
|
<Callout type="info">
|
|
258
258
|
See the full API reference for [`createWebhook()`](/docs/api-reference/workflow/create-webhook) for all available options.
|
|
259
259
|
</Callout>
|
|
260
260
|
|
|
261
|
-
### Creating
|
|
261
|
+
### Creating your first webhook
|
|
262
262
|
|
|
263
|
-
Here's a
|
|
263
|
+
Here's a webhook that receives HTTP requests. Like hooks, webhooks support the `using` keyword for automatic cleanup:
|
|
264
264
|
|
|
265
265
|
```typescript lineNumbers
|
|
266
266
|
import { createWebhook } from "workflow";
|
|
@@ -284,13 +284,13 @@ export async function webhookWorkflow() {
|
|
|
284
284
|
}
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
The webhook will automatically respond with a `202 Accepted` status by default. External systems can
|
|
287
|
+
The webhook will automatically respond with a `202 Accepted` status by default. External systems can make an HTTP request to the `webhook.url` to resume your workflow.
|
|
288
288
|
|
|
289
|
-
### Sending
|
|
289
|
+
### Sending custom responses
|
|
290
290
|
|
|
291
291
|
Webhooks provide two ways to send custom HTTP responses: **static responses** and **dynamic responses**.
|
|
292
292
|
|
|
293
|
-
#### Static
|
|
293
|
+
#### Static responses
|
|
294
294
|
|
|
295
295
|
Use the `respondWith` option to provide a static response that will be sent automatically for every request:
|
|
296
296
|
|
|
@@ -319,7 +319,7 @@ async function processData(data: any) {
|
|
|
319
319
|
}
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
-
#### Dynamic
|
|
322
|
+
#### Dynamic responses (manual mode)
|
|
323
323
|
|
|
324
324
|
For dynamic responses based on the request content, set `respondWith: "manual"` and call the `respondWith()` method on the request:
|
|
325
325
|
|
|
@@ -365,7 +365,7 @@ export async function webhookWithDynamicResponse() {
|
|
|
365
365
|
When using `respondWith: "manual"`, the `respondWith()` method **must** be called from within a step function due to serialization requirements. This requirement may be removed in the future.
|
|
366
366
|
</Callout>
|
|
367
367
|
|
|
368
|
-
### Handling
|
|
368
|
+
### Handling multiple webhook requests
|
|
369
369
|
|
|
370
370
|
Like hooks, webhooks support iteration:
|
|
371
371
|
|
|
@@ -405,7 +405,7 @@ export async function eventCollectorWorkflow() {
|
|
|
405
405
|
}
|
|
406
406
|
```
|
|
407
407
|
|
|
408
|
-
## Hooks vs.
|
|
408
|
+
## Hooks vs. webhooks: when to use each
|
|
409
409
|
|
|
410
410
|
| Feature | Hooks | Webhooks |
|
|
411
411
|
|---------|-------|----------|
|
|
@@ -415,19 +415,19 @@ export async function eventCollectorWorkflow() {
|
|
|
415
415
|
| **Use Case** | Custom integrations, type-safe payloads | HTTP webhooks, standard REST APIs |
|
|
416
416
|
| **Resuming** | [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) | Automatic via HTTP, or [`resumeWebhook()`](/docs/api-reference/workflow-api/resume-webhook) |
|
|
417
417
|
|
|
418
|
-
**Use
|
|
418
|
+
**Use hooks when:**
|
|
419
419
|
- You need full control over the payload structure
|
|
420
420
|
- You're integrating with custom event sources
|
|
421
421
|
- You want strong TypeScript typing with [`defineHook()`](/docs/api-reference/workflow/define-hook)
|
|
422
422
|
|
|
423
|
-
**Use
|
|
423
|
+
**Use webhooks when:**
|
|
424
424
|
- You're receiving HTTP requests from external services
|
|
425
425
|
- You need to send HTTP responses back to the caller
|
|
426
426
|
- You want automatic URL routing without writing API handlers
|
|
427
427
|
|
|
428
|
-
## Advanced
|
|
428
|
+
## Advanced patterns
|
|
429
429
|
|
|
430
|
-
### Type-
|
|
430
|
+
### Type-safe hooks with `defineHook()`
|
|
431
431
|
|
|
432
432
|
The [`defineHook()`](/docs/api-reference/workflow/define-hook) helper provides type safety and runtime validation between creating and resuming hooks using [Standard Schema v1](https://standardschema.dev). Use any compliant validator like Zod or Valibot:
|
|
433
433
|
|
|
@@ -476,25 +476,25 @@ export async function POST(request: Request) {
|
|
|
476
476
|
|
|
477
477
|
This pattern is especially valuable in larger applications where the workflow and API code are in separate files, providing both compile-time type safety and runtime validation.
|
|
478
478
|
|
|
479
|
-
## Best
|
|
479
|
+
## Best practices
|
|
480
480
|
|
|
481
|
-
### Token
|
|
481
|
+
### Token design
|
|
482
482
|
|
|
483
|
-
Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always
|
|
483
|
+
Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always generate their own unique tokens. A generated token is not trivial to guess, but it is not a strong security contract either, so anyone who obtains the URL can invoke an unintended webhook resumption. To prevent unauthenticated run resumptions entirely, prefer a **hook** over the **webhook** convenience and implement your own authentication on the route that calls `resumeHook()`.
|
|
484
484
|
|
|
485
485
|
When using custom tokens with `createHook()`:
|
|
486
486
|
|
|
487
|
-
- **Make them deterministic**: Base them on data the external system can reconstruct
|
|
488
|
-
- **Use namespacing**: Prefix tokens to avoid conflicts
|
|
489
|
-
- **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance
|
|
487
|
+
- **Make them deterministic**: Base them on data the external system can reconstruct, such as channel IDs or user IDs.
|
|
488
|
+
- **Use namespacing**: Prefix tokens to avoid conflicts, such as `slack:${channelId}` or `github:${repoId}`.
|
|
489
|
+
- **Include routing information**: Ensure the token contains enough information to identify the correct workflow instance.
|
|
490
490
|
|
|
491
|
-
### Response
|
|
491
|
+
### Response handling in webhooks
|
|
492
492
|
|
|
493
|
-
- Use **static responses** (`respondWith: Response`) for
|
|
493
|
+
- Use **static responses** (`respondWith: Response`) for acknowledgments
|
|
494
494
|
- Use **manual mode** (`respondWith: "manual"`) when responses depend on request processing
|
|
495
495
|
- Remember that `respondWith()` must be called from within a step function
|
|
496
496
|
|
|
497
|
-
### Iterating
|
|
497
|
+
### Iterating over events
|
|
498
498
|
|
|
499
499
|
Both hooks and webhooks support iteration, making them perfect for long-running event loops:
|
|
500
500
|
|
|
@@ -513,7 +513,7 @@ for await (const event of hook) {
|
|
|
513
513
|
|
|
514
514
|
This pattern allows a single workflow instance to handle multiple events over time, maintaining state between events.
|
|
515
515
|
|
|
516
|
-
## Related
|
|
516
|
+
## Related documentation
|
|
517
517
|
|
|
518
518
|
- [Serialization](/docs/foundations/serialization) - Understanding what data can be passed through hooks
|
|
519
519
|
- [`createHook()` API Reference](/docs/api-reference/workflow/create-hook)
|
|
@@ -15,9 +15,9 @@ Idempotency is a property of an operation that ensures repeated attempts have th
|
|
|
15
15
|
|
|
16
16
|
In Workflow, idempotency shows up in two related places: step idempotency makes external calls safe when a step retries, and run idempotency coordinates duplicate requests that try to start the same workflow.
|
|
17
17
|
|
|
18
|
-
## Step
|
|
18
|
+
## Step idempotency
|
|
19
19
|
|
|
20
|
-
In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once
|
|
20
|
+
In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once by seeing if it succeeds.
|
|
21
21
|
Consider a payment API that charges the user $10, but due to network failures, the confirmation response is lost. When the step retries (because the previous attempt was considered a failure), it will charge the user again.
|
|
22
22
|
|
|
23
23
|
To prevent this, many external APIs support idempotency keys. An idempotency key is a unique identifier for an operation that can be used to deduplicate requests.
|
|
@@ -61,7 +61,7 @@ Because [hooks](/docs/foundations/hooks) already ensure globally unique active t
|
|
|
61
61
|
|
|
62
62
|
Use a hook token as the idempotency key for an active workflow run. Hook tokens are globally unique while they are active: if another run tries to create a hook with the same token, the runtime records a conflict, `hook.getConflict()` resolves with a `Run` handle for the run that owns the token, and the hook rejects with [`HookConflictError`](/docs/errors/hook-conflict) when the workflow awaits or iterates its payload.
|
|
63
63
|
|
|
64
|
-
The token should come from your domain, such as an order ID, invoice ID, import ID, or request ID. Create the hook near the beginning of the workflow and check `await hook.getConflict()` before doing duplicate-sensitive work that depends on owning the active token. Calling `createHook()` alone does not register the hook
|
|
64
|
+
The token should come from your domain, such as an order ID, invoice ID, import ID, or request ID. Create the hook near the beginning of the workflow and check `await hook.getConflict()` before doing duplicate-sensitive work that depends on owning the active token. Calling `createHook()` alone does not register the hook; awaiting `getConflict()` suspends the workflow to commit the registration.
|
|
65
65
|
|
|
66
66
|
```typescript lineNumbers
|
|
67
67
|
import { createHook } from "workflow";
|
|
@@ -96,7 +96,7 @@ export async function processOrder(orderId: string): Promise<OrderResult> {
|
|
|
96
96
|
}
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
The runtime creates the hook atomically. At most one hook can own `order:${orderId}`, so duplicate workflow runs converge on one owner. A duplicate run observes `getConflict()` resolving with the owner's `Run` and returns before it reaches `chargeOrder()`. The conflicting run's accessors (`status`, `returnValue`, `cancel()`, …) are durable steps, so the duplicate run can do more than report the owner
|
|
99
|
+
The runtime creates the hook atomically. At most one hook can own `order:${orderId}`, so duplicate workflow runs converge on one owner. A duplicate run observes `getConflict()` resolving with the owner's `Run` and returns before it reaches `chargeOrder()`. The conflicting run's accessors (`status`, `returnValue`, `cancel()`, …) are durable steps, so the duplicate run can do more than report the owner. See [conflict-handling strategies](#conflict-handling-strategies) below.
|
|
100
100
|
|
|
101
101
|
Outside the workflow, try to resume the hook first. If the hook is not registered yet, start the workflow and retry the resume until the new run creates the hook:
|
|
102
102
|
|
|
@@ -146,14 +146,14 @@ export async function POST(request: Request) {
|
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
<Callout type="warn">
|
|
149
|
-
This avoids creating a new run only after the first run has registered its hook. Because `start()` returns before the run body executes and calls `createHook()`, two concurrent requests can both observe "no hook yet" and each call `start()`. The race is resolved inside the workflow body, where the losing run observes `getConflict()` resolving with the active owner and returns without doing duplicate-sensitive work
|
|
149
|
+
This avoids creating a new run only after the first run has registered its hook. Because `start()` returns before the run body executes and calls `createHook()`, two concurrent requests can both observe "no hook yet" and each call `start()`. The race is resolved inside the workflow body, where the losing run observes `getConflict()` resolving with the active owner and returns without doing duplicate-sensitive work, and the route detects it by comparing the resumed hook's `runId` against the run it just started, without waiting for either run to finish. A native API for atomically starting a run and registering a hook is in the works. Until then, model recovery inside the workflow by checking `hook.getConflict()`.
|
|
150
150
|
</Callout>
|
|
151
151
|
|
|
152
152
|
This coordinates active runs by default: the token becomes available when its workflow ends. Set `experimental_minRetention` to keep it unavailable to late duplicates. After the workflow ends, the Hook can still be found with `getHookByToken()` until retention ends, but it cannot be resumed. See [`createHook()` minimum retention](/docs/api-reference/workflow/create-hook#keep-a-token-unavailable-after-the-run-ends) for examples and supported values.
|
|
153
153
|
|
|
154
154
|
### Conflict-handling strategies
|
|
155
155
|
|
|
156
|
-
Some workflow systems resolve duplicate IDs with a fixed, pre-declared policy
|
|
156
|
+
Some workflow systems resolve duplicate IDs with a fixed, pre-declared policy, typically a static choice between rejecting the new execution, deferring to the existing one, or terminating it. Workflow has no policy enum. `hook.getConflict()` hands the duplicate run the conflicting `Run` itself, and the policy is ordinary code, including policies that inspect state before deciding, which static configuration can't express.
|
|
157
157
|
|
|
158
158
|
The example above implements **reject the duplicate**: return the owner's `runId` and let the caller decide. Other common strategies:
|
|
159
159
|
|
|
@@ -260,7 +260,7 @@ export async function processOrderNewestWins(orderId: string) {
|
|
|
260
260
|
|
|
261
261
|
const conflict = await request.getConflict();
|
|
262
262
|
if (!conflict) {
|
|
263
|
-
// Token claimed
|
|
263
|
+
// Token claimed: this run is now the owner.
|
|
264
264
|
const { confirmed } = await request;
|
|
265
265
|
if (confirmed) {
|
|
266
266
|
await chargeOrder(orderId);
|
|
@@ -271,12 +271,12 @@ export async function processOrderNewestWins(orderId: string) {
|
|
|
271
271
|
await conflict.cancel(); // [!code highlight]
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
throw new Error(`Could not claim ${token} after
|
|
274
|
+
throw new Error(`Could not claim ${token} after canceling the owner`);
|
|
275
275
|
}
|
|
276
276
|
```
|
|
277
277
|
|
|
278
278
|
<Callout type="warn">
|
|
279
|
-
This pattern does not work with `experimental_minRetention`:
|
|
279
|
+
This pattern does not work with `experimental_minRetention`: canceling the old run does not make its token available early.
|
|
280
280
|
</Callout>
|
|
281
281
|
|
|
282
282
|
If duplicate requests should only reuse the active run without sending data, use [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) as an advisory pre-check before calling `start()`. The workflow should still check `hook.getConflict()`, because the lookup and `start()` are not atomic.
|