workflow 5.0.0-beta.44 → 5.0.0-beta.46
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/docs/api-reference/workflow-api/resume-hook.mdx +3 -3
- package/docs/api-reference/workflow-api/resume-webhook.mdx +1 -1
- package/docs/changelog/attributes-mvp.mdx +2 -2
- package/docs/changelog/index.mdx +1 -0
- package/docs/changelog/lazy-hook-resume.mdx +51 -0
- package/docs/changelog/meta.json +1 -0
- package/docs/changelog/resilient-resume.mdx +8 -0
- package/docs/configuration/runtime-tuning.mdx +4 -3
- package/docs/whats-new.mdx +1 -1
- package/package.json +10 -10
|
@@ -12,9 +12,9 @@ related:
|
|
|
12
12
|
|
|
13
13
|
Resumes a workflow run by sending a payload to a hook identified by its token.
|
|
14
14
|
|
|
15
|
-
It
|
|
15
|
+
It publishes a workflow invocation carrying the payload; the runtime creates the `hook_received` event and continues execution from it.
|
|
16
16
|
|
|
17
|
-
A Hook kept by `experimental_minRetention
|
|
17
|
+
`resumeHook()` throws `HookNotFoundError` when no hook holds the token. A run that has already ended cannot be resumed, including one whose Hook is kept by `experimental_minRetention`, but whether the call reports that depends on the path it takes: a resume dispatched without reading the run resolves and the ended state is only detected once the payload arrives, while one that reads the run, or that falls back to writing the event up front, throws `HookNotFoundError`. See [lazy hook resume](/docs/changelog/lazy-hook-resume).
|
|
18
18
|
|
|
19
19
|
<Callout type="warn">
|
|
20
20
|
`resumeHook` is a runtime function that must be called from outside a workflow function.
|
|
@@ -50,7 +50,7 @@ showSections={["parameters"]}
|
|
|
50
50
|
|
|
51
51
|
### Returns
|
|
52
52
|
|
|
53
|
-
Returns a `Promise<ResumedHook>`, a `Hook` extended with an optional `resilientResume` flag. Resolving means the resume was accepted
|
|
53
|
+
Returns a `Promise<ResumedHook>`, a `Hook` extended with an optional `resilientResume` flag. Resolving means the resume was accepted for delivery: the payload rides the workflow queue message and the runtime materializes the `hook_received` event from it before replaying (see the [lazy hook resume changelog](/docs/changelog/lazy-hook-resume)). `resilientResume` is retained for source compatibility and is no longer set by any path. The resolved hook:
|
|
54
54
|
|
|
55
55
|
<TSDoc
|
|
56
56
|
definition={`
|
|
@@ -11,7 +11,7 @@ related:
|
|
|
11
11
|
|
|
12
12
|
Resumes a workflow run by sending an HTTP `Request` to a webhook identified by its token.
|
|
13
13
|
|
|
14
|
-
This function
|
|
14
|
+
This function publishes a workflow invocation carrying the request; the runtime creates the `hook_received` event from it and continues execution. It's designed to be called from API routes or server actions that receive external HTTP requests.
|
|
15
15
|
|
|
16
16
|
<Callout type="warn">
|
|
17
17
|
`resumeWebhook` is a runtime function that must be called from outside a workflow function.
|
|
@@ -273,7 +273,7 @@ If you need behavior the MVP does not provide (read, list, filter, initial attri
|
|
|
273
273
|
Unit tests in `@workflow/world` (validation surface) and `@workflow/core` (VM-side dispatch + host-side stub):
|
|
274
274
|
|
|
275
275
|
- Validation rules: key length, value byte cap, `$` prefix, per-batch duplicates, post-merge count cap (with `existingKeys` so updates of present keys don't falsely trip the cap)
|
|
276
|
-
- Reserved `$` namespace: rejected by default
|
|
276
|
+
- Reserved `$` namespace: rejected by default and accepted by the contextual `validateAttributeChanges` check when `allowReservedAttributes: true` is passed
|
|
277
277
|
- `experimental_setAttributes({})` is a no-op (no dispatch, no events)
|
|
278
278
|
- `undefined` value normalizes to a `null`-valued change on the wire
|
|
279
279
|
- The `{ allowReservedAttributes: true }` opt-in is forwarded through the step bridge so the world receives the flag
|
|
@@ -367,7 +367,7 @@ For the MVP the endpoint reuses the existing `WORKFLOW_EVENT` fact with `eventTy
|
|
|
367
367
|
|
|
368
368
|
### Validation rules are shared between SDK and world
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
Context-free validation lives in the exported Zod schemas (`AttributeKeySchema`, `AttributeValueSchema`, `AttributeChangeSchema`, and `AttributeChangesSchema`). The schema-free `validateAttributeChanges` helper adds rules that depend on caller context, including the post-merge count and reserved `$` namespace. Both the SDK `experimental_setAttributes` helper and the `world-local` / `world-postgres` implementations call it; the `world-vercel` backing service applies the same rules independently. The shared module remains the authoritative spec for the limits (256-char keys, 256-byte values, max 64 attributes per run, `$`-prefixed keys reserved).
|
|
371
371
|
|
|
372
372
|
### Run row reconstruction had to thread `attributes` through
|
|
373
373
|
|
package/docs/changelog/index.mdx
CHANGED
|
@@ -12,6 +12,7 @@ Stay up to date with the latest changes to Workflow SDK.
|
|
|
12
12
|
|
|
13
13
|
## 2026
|
|
14
14
|
|
|
15
|
+
- [Lazy hook resume](/docs/changelog/lazy-hook-resume) (August 2026)
|
|
15
16
|
- [Resilient hook resume](/docs/changelog/resilient-resume) (July 2026)
|
|
16
17
|
- [Eager processing of steps and incremental event replay](/docs/changelog/eager-processing) (March 2026)
|
|
17
18
|
- Serializable AbortController and AbortSignal (March 12, 2026)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Lazy hook resume
|
|
3
|
+
description: resumeHook() no longer writes hook_received itself. The queue consumer materializes the event from the message, so a resume costs one round trip.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Lazy hook resume
|
|
7
|
+
|
|
8
|
+
## Motivation
|
|
9
|
+
|
|
10
|
+
[Resilient hook resume](/docs/changelog/resilient-resume) made `resumeHook()` write the `hook_received` event and publish the workflow queue message concurrently, with the queue consumer re-ensuring the event from the message's `hookInput` before replay. Both sides then wrote the same event, and a `(runId, resumeId)` constraint collapsed them onto one.
|
|
11
|
+
|
|
12
|
+
Running the two concurrently removed the second round trip from the critical path, but the write itself stayed: every resume still spent a request on an event the consumer was about to write anyway, and the producer still had to classify its outcome (conflict, throttle, terminal run) to decide whether the resume had survived.
|
|
13
|
+
|
|
14
|
+
This change drops the producer's write entirely. On the lazy path `resumeHook()` publishes the queue message and nothing else.
|
|
15
|
+
|
|
16
|
+
## Design
|
|
17
|
+
|
|
18
|
+
- `resumeHook()` publishes one message carrying `hookInput`: the dehydrated payload, a client-minted `resumeId`, the hook token, and a payload digest. It writes no event.
|
|
19
|
+
- The queue consumer materializes `hook_received` from `hookInput` before replay, keyed by `resumeId`. This is the same write it already performed; it is now the only one.
|
|
20
|
+
- The `(runId, resumeId)` constraint still matters: a queue redelivery, or a delivery re-routed for deployment affinity, repeats the write with the same key, and the backend collapses those onto exactly one committed event.
|
|
21
|
+
- **A failed publish fails the resume.** The message carries both the trigger and the only copy of the payload, so `resumeHook()` throws and nothing is persisted for a later delivery to pick up. This replaces the previous rule where a failed event write could still be recovered through the queue.
|
|
22
|
+
- `ResumedHook.resilientResume` is retained on the type but is never set: with a single writer there is no partial outcome to report. The `workflow.hook.resilient_resume` span attribute is likewise no longer emitted.
|
|
23
|
+
- The resume span reports `workflow.hook.resume_strategy: lazy` (previously `parallel`).
|
|
24
|
+
|
|
25
|
+
## Behavior change: the event is not visible when `resumeHook()` returns
|
|
26
|
+
|
|
27
|
+
`resumeHook()` used to await its own `hook_received` write, so by the time it resolved the event was in the log. It no longer writes, so **resolving means the message was published, not that the event exists**. The event appears when the run picks the resume up.
|
|
28
|
+
|
|
29
|
+
Code that reads the run back immediately after resuming now races. The pattern that breaks is a loop that resumes and then looks for the next thing to resume, keying off "this hook has no `hook_received` yet": it can be handed back the hook it just resumed and deliver a second payload to it. Wait for something that implies the run made progress instead. `waitForHook()` in `@workflow/vitest` takes a `notHookId` option for exactly this.
|
|
30
|
+
|
|
31
|
+
The runtime has one caller that needs the old guarantee. A step that aborts a shared `AbortController` resumes a hook to record the abort in the event log, and that write is an ordering barrier: it must land before the step completes, or the continuation `step_completed` enqueues can dispatch the next step with a stale, non-aborted signal. That path uses an internal durable resume which keeps the eager write and reports `resume_fallback_reason: durable_required`.
|
|
32
|
+
|
|
33
|
+
Nothing about delivery changes. The payload is on the queue message and reaches the workflow exactly once.
|
|
34
|
+
|
|
35
|
+
## Behavior change: resumes against an ended run
|
|
36
|
+
|
|
37
|
+
The hook lookup is unchanged: `resumeHook(token, ...)` still resolves the token through `hooks.getByToken()`, which throws `HookNotFoundError` when no hook holds it. Hook existence, and the token's binding to a run, are still validated before anything is published.
|
|
38
|
+
|
|
39
|
+
What the lookup does not carry is the run's *mutable* status. `HookResumeContext` is deliberately an immutable slice of the run, so a resume that runs off it never learns whether the run is still live. That used to be caught by the `hook_received` write being rejected. With no write, **a resume against an ended run resolves instead of throwing `HookNotFoundError`**.
|
|
40
|
+
|
|
41
|
+
This is only reachable when the hook record outlives its run, since otherwise the lookup itself fails: a hook kept by `experimental_minRetention`, or one whose token has not been released yet. Resumes that fall back to reading the run keep their terminal pre-check, as does the sequential path, so a resume on either of those still fails loudly.
|
|
42
|
+
|
|
43
|
+
Nothing resumes either way. The consumer's write is rejected the same way and the delivery is consumed, so the ended run is untouched. Only the producer's report changes: an accepted publish means the resume was dispatched, not that the run was still live when it arrived. A [webhook](/docs/api-reference/workflow-api/resume-webhook) whose run has ended can answer `202` rather than surfacing an error.
|
|
44
|
+
|
|
45
|
+
Callers that need the distinction have to read the run.
|
|
46
|
+
|
|
47
|
+
## Compatibility
|
|
48
|
+
|
|
49
|
+
The gating is unchanged: the lazy path activates only when the target run's queue consumer and the live backend both attest support, re-checked on every resume. Oversized payloads, legacy runs, non-CBOR transports, and `WORKFLOW_DISABLE_LAZY_HOOK_RESUME=1` fall back to the sequential write-then-publish path.
|
|
50
|
+
|
|
51
|
+
Consumers still accept a message from an older producer that wrote the event itself: such a message reports `strategy: parallel`, and the consumer's write converges on the producer's committed event exactly as before. No coordinated deploy is needed in either direction.
|
package/docs/changelog/meta.json
CHANGED
|
@@ -5,6 +5,14 @@ description: resumeHook() now tolerates transient event storage failures when th
|
|
|
5
5
|
|
|
6
6
|
# Resilient `resumeHook()`
|
|
7
7
|
|
|
8
|
+
<Callout type="info">
|
|
9
|
+
Superseded by [lazy hook resume](/docs/changelog/lazy-hook-resume):
|
|
10
|
+
`resumeHook()` no longer writes the `hook_received` event at all on the fast
|
|
11
|
+
path, so the two-writer design and the `resilientResume` flag described below
|
|
12
|
+
are historical. The `(runId, resumeId)` constraint and the queue-carried
|
|
13
|
+
payload remain.
|
|
14
|
+
</Callout>
|
|
15
|
+
|
|
8
16
|
## Motivation
|
|
9
17
|
|
|
10
18
|
`resumeHook()` used to write the `hook_received` event and dispatch the workflow queue message strictly one after the other, so every resume paid two sequential round trips and a transient event-storage failure failed the whole resume even when the queue was healthy. This change runs both writes **concurrently** (cutting a round trip off resume latency) and, on the same path, brings `resumeHook()` to parity with [resilient `start()`](/docs/changelog/resilient-start): a transient event-write failure no longer fails the resume when the payload can still be delivered through the queue.
|
|
@@ -84,8 +84,9 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
84
84
|
### `WORKFLOW_DISABLE_LAZY_HOOK_RESUME`
|
|
85
85
|
|
|
86
86
|
- Default: enabled (lazy hook resume on)
|
|
87
|
-
- Resuming a hook
|
|
88
|
-
- The runtime falls back to the sequential path when the consumer or backend does not attest dedup support (or the payload is too large to inline on the queue message). On the sequential path, the event is written *before* dispatch, and its failure fails the resume.
|
|
87
|
+
- Resuming a hook publishes the workflow invocation and writes no event, so the resume costs one round trip. The queue message carries the payload, and the consumer creates the `hook_received` event from it before replay. A backend `(runId, resumeId)` constraint keeps redeliveries of that message converging on exactly one event. Because the message is the only copy of the payload, a failed publish fails the resume.
|
|
88
|
+
- The runtime falls back to the sequential path when the consumer or backend does not attest dedup support (or the payload is too large to inline on the queue message). On the sequential path, the event is written *before* dispatch, and its failure fails the resume.
|
|
89
|
+
- The two paths differ on ended runs: the sequential write is rejected and surfaces as `HookNotFoundError`, while the lazy path resolves because it never writes. Neither resumes the run.
|
|
89
90
|
- Set `1` to force the sequential path as a kill switch. The chosen strategy is reported on the resume span as `workflow.hook.resume_strategy`.
|
|
90
91
|
|
|
91
92
|
### `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES`
|
|
@@ -193,7 +194,7 @@ For example, a workflow can run a 10-minute inline step even with `WORKFLOW_REPL
|
|
|
193
194
|
|
|
194
195
|
- Default: enabled
|
|
195
196
|
- Keeps the suspended workflow VM alive across inline steps within one invocation, so each iteration of the inline loop appends only the newly written events instead of replaying the whole event log in a fresh VM.
|
|
196
|
-
- Suspensions involving
|
|
197
|
+
- A step-driven suspension can keep the VM retained even when hooks are open or created at the same boundary. Hook-only suspensions park the invocation. Suspensions involving waits or attributes, runs with an open wait, and any replay divergence fall back to a full replay.
|
|
197
198
|
- Step inputs made of plain data (objects, arrays, primitives) and standard built-ins (`Map`, `Set`, `Date`, `RegExp`, typed arrays, `ArrayBuffer`, `URL`, `Headers`) keep the VM retained. Patching or polyfilling built-in prototypes doesn't change that because serialization never calls them. A boundary falls back to a full replay only when serializing its arguments runs code the workflow controls, such as a getter, a proxy, or a custom class serializer, or computes an `Error`'s stack trace.
|
|
198
199
|
- Set `0` or `false` to replay from scratch in a fresh VM on every iteration.
|
|
199
200
|
|
package/docs/whats-new.mdx
CHANGED
|
@@ -33,7 +33,7 @@ The largest change in v5 has no API surface: the runtime does far less work per
|
|
|
33
33
|
|
|
34
34
|
**The runtime avoids waiting on the persistence layer where it can determine that is safe for your workload.** The runtime skips many API calls when they aren't needed, such as requesting the event log on a run's first invocation. Step creation is folded into step execution rather than being its own round trip. The inline loop consumes the event-log delta from the previous step's write instead of re-listing events. Each optimization is gated on specific runtime conditions and can be turned off individually. See [Runtime tuning](/docs/configuration/runtime-tuning).
|
|
35
35
|
|
|
36
|
-
**The workflow VM is kept alive across inline steps.** Within one invocation, a step-
|
|
36
|
+
**The workflow VM is kept alive across inline steps.** Within one invocation, a step-driven suspension keeps the live VM and hydrated state, including when hooks are open or created at the same boundary, so the next iteration appends only the newly written events instead of rebuilding the sandbox and replaying the whole log. Step inputs made of plain data or standard built-ins keep this fast path; see [`WORKFLOW_RETAINED_VM`](/docs/configuration/runtime-tuning#workflow_retained_vm).
|
|
37
37
|
|
|
38
38
|
**Resuming a hook takes one round trip instead of two.** `resumeHook()` writes the `hook_received` event and dispatches the queue message concurrently, with a `(runId, resumeId)` dedup constraint keeping the two writers converging on exactly one event. See [Resilient hook resumption](/docs/changelog/resilient-resume).
|
|
39
39
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.46",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -59,18 +59,18 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"ms": "2.1.3",
|
|
62
|
-
"@workflow/astro": "5.0.0-beta.
|
|
63
|
-
"@workflow/cli": "5.0.0-beta.
|
|
64
|
-
"@workflow/core": "5.0.0-beta.
|
|
62
|
+
"@workflow/astro": "5.0.0-beta.46",
|
|
63
|
+
"@workflow/cli": "5.0.0-beta.46",
|
|
64
|
+
"@workflow/core": "5.0.0-beta.46",
|
|
65
65
|
"@workflow/errors": "5.0.0-beta.18",
|
|
66
66
|
"@workflow/typescript-plugin": "5.0.0-beta.5",
|
|
67
67
|
"@workflow/utils": "5.0.0-beta.9",
|
|
68
|
-
"@workflow/next": "5.0.0-beta.
|
|
69
|
-
"@workflow/nest": "5.0.0-beta.
|
|
70
|
-
"@workflow/nitro": "5.0.0-beta.
|
|
71
|
-
"@workflow/nuxt": "5.0.0-beta.
|
|
72
|
-
"@workflow/sveltekit": "5.0.0-beta.
|
|
73
|
-
"@workflow/rollup": "5.0.0-beta.
|
|
68
|
+
"@workflow/next": "5.0.0-beta.46",
|
|
69
|
+
"@workflow/nest": "5.0.0-beta.46",
|
|
70
|
+
"@workflow/nitro": "5.0.0-beta.46",
|
|
71
|
+
"@workflow/nuxt": "5.0.0-beta.46",
|
|
72
|
+
"@workflow/sveltekit": "5.0.0-beta.46",
|
|
73
|
+
"@workflow/rollup": "5.0.0-beta.46"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/ms": "2.1.0",
|