workflow 5.0.0-beta.13 → 5.0.0-beta.15

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.
Files changed (68) hide show
  1. package/docs/api-reference/index.mdx +24 -0
  2. package/docs/api-reference/meta.json +8 -0
  3. package/docs/api-reference/vitest/index.mdx +0 -6
  4. package/docs/api-reference/workflow/create-hook.mdx +32 -0
  5. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  6. package/docs/api-reference/workflow/experimental-set-attributes.mdx +3 -1
  7. package/docs/api-reference/workflow-api/index.mdx +6 -8
  8. package/docs/api-reference/workflow-api/start.mdx +3 -1
  9. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  10. package/docs/api-reference/workflow-astro/meta.json +4 -0
  11. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  12. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  13. package/docs/api-reference/workflow-errors/meta.json +5 -0
  14. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  15. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  16. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  17. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  18. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  19. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  20. package/docs/api-reference/workflow-nest/meta.json +9 -0
  21. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  22. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  23. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  24. package/docs/api-reference/workflow-nitro/index.mdx +59 -0
  25. package/docs/api-reference/workflow-nuxt/index.mdx +47 -0
  26. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  27. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  28. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  29. package/docs/api-reference/workflow-observability/meta.json +11 -0
  30. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  31. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  32. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  33. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  34. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  35. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  36. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  37. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  38. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  39. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  40. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  41. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  42. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
  43. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  44. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +2 -2
  45. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +5 -4
  46. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  47. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  48. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  49. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
  50. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  51. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  52. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  53. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  54. package/docs/api-reference/workflow-vite/meta.json +4 -0
  55. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  56. package/docs/changelog/attributes-mvp.mdx +15 -3
  57. package/docs/changelog/eager-processing.mdx +26 -6
  58. package/docs/errors/index.mdx +3 -0
  59. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  60. package/docs/foundations/hooks.mdx +29 -0
  61. package/docs/foundations/streaming.mdx +1 -1
  62. package/docs/foundations/versioning.mdx +1 -1
  63. package/docs/how-it-works/encryption.mdx +2 -2
  64. package/docs/how-it-works/event-sourcing.mdx +2 -2
  65. package/docs/observability/attributes.mdx +12 -5
  66. package/package.json +10 -10
  67. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  68. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: workflow
3
+ description: Configure Vite and Nitro to transform workflow directives.
4
+ type: reference
5
+ summary: Add the workflow plugin to your Vite config to enable workflow directive transformation in Vite + Nitro apps.
6
+ prerequisites:
7
+ - /docs/getting-started/vite
8
+ ---
9
+
10
+ Returns the Vite plugins that transform workflow code (`"use step"`/`"use workflow"` directives) and configure the [`workflow/nitro`](/docs/api-reference/workflow-nitro) module on the Nitro server. It is designed to be used alongside `nitro()` from `nitro/vite`, which provides the server framework for API routes and deployment.
11
+
12
+ ## Usage
13
+
14
+ To enable `"use step"` and `"use workflow"` directives while developing locally or deploying to production, add `workflow()` to the `plugins` array of your Vite config, together with `nitro()`.
15
+
16
+ ```typescript title="vite.config.ts" lineNumbers
17
+ import { nitro } from "nitro/vite";
18
+ import { defineConfig } from "vite";
19
+ import { workflow } from "workflow/vite"; // [!code highlight]
20
+
21
+ export default defineConfig({
22
+ plugins: [nitro(), workflow()], // [!code highlight]
23
+ nitro: {
24
+ serverDir: "./",
25
+ },
26
+ });
27
+ ```
28
+
29
+ ## API Signature
30
+
31
+ ### Parameters
32
+
33
+ | Parameter | Type | Description |
34
+ | --- | --- | --- |
35
+ | `options` | `ModuleOptions` | Optional. Forwarded to the `workflow/nitro` module as its module options. |
36
+
37
+ #### ModuleOptions
38
+
39
+ | Option | Type | Default | Description |
40
+ | --- | --- | --- | --- |
41
+ | `dirs` | `string[]` | — | Directories to scan for workflows and steps. By default, the `workflows/` directory is scanned from the project root and all layer source directories. |
42
+ | `typescriptPlugin` | `boolean` | `false` | Adds the `workflow` TypeScript plugin to the generated `tsconfig.json` for IDE IntelliSense. |
43
+ | `runtime` | `string` | — | Node.js runtime version for Vercel Functions (e.g. `'nodejs22.x'`, `'nodejs24.x'`). Only applies when deploying to Vercel. |
44
+ | `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` | Controls source maps on generated workflow bundles. Accepts the same values as esbuild's `sourcemap` option. Set to `false` for smaller function bundles (useful for staying under the Vercel 250MB function size limit) at the cost of stack traces pointing at generated code. Can also be set via the `WORKFLOW_SOURCEMAP` environment variable. |
45
+
46
+ ### Returns
47
+
48
+ Returns an array of Vite `Plugin` objects. Spread or pass the array directly to the `plugins` option of your Vite config — Vite flattens nested plugin arrays automatically.
@@ -9,6 +9,18 @@ This is a minimal, **experimental** subset of the [planned Workflow Attributes f
9
9
 
10
10
  The MVP lets workflow code attach plaintext `string → string` metadata to a run, viewable in any observability surface that reads the `WorkflowRun` entity. It is deliberately narrow: write-only, no reads from inside a run, no list/filter endpoints, no event-log representation. The wire format and SDK surface are chosen so the full 5.0.0 implementation replaces this without source-level breaking changes for end users.
11
11
 
12
+ ## Native v4 follow-up
13
+
14
+ The v4 implementation now completes the planned dispatch migration while preserving the experimental SDK name:
15
+
16
+ - `experimental_setAttributes()` appends a native `attr_set` event from workflow and step bodies instead of mutating through the internal step bridge.
17
+ - Native events carry writer attribution and are materialized immediately onto `run.attributes`.
18
+ - `start(workflow, args, { attributes })` seeds attributes on the initial `run_created` event, including the resilient-start creation path.
19
+ - The World spec is bumped to v4; attributes are supported for v4 runs rather than being backported into older run specifications.
20
+ - The direct server attributes endpoint remains available only as a deprecated compatibility path for clients that shipped with the MVP.
21
+
22
+ The remainder of this page documents the original MVP motivation and implementation.
23
+
12
24
  ## What MVP supports
13
25
 
14
26
  - `experimental_setAttributes(record)` callable from a **workflow body** (`"use workflow"` function), dispatched via an internal `__builtin_set_attributes` step bridge so the mutation gets a `step_created → step_completed` event pair
@@ -296,13 +308,13 @@ End-to-end in `workbench/nextjs-turbopack` (exercises the full SWC plugin + work
296
308
  - `Promise.all` of disjoint-key writes — every key persists
297
309
  - Workflow throws after an awaited `experimental_setAttributes` — the attribute persists on the now-`failed` run (the per-run file lock on `run_failed` re-reads inside the critical section so the attribute snapshot survives the lifecycle write)
298
310
 
299
- ## Migration to 5.0.0
311
+ ## Migration to native v4
300
312
 
301
- When the full attributes feature ships:
313
+ The native attributes follow-up applies this migration:
302
314
 
303
315
  - `experimental_setAttributes` (SDK) — unchanged signature, new dispatch path
304
316
  - `runs.experimentalSetAttributes` (world interface) — deprecated, then removed; replaced by `events.create(runId, { eventType: 'attr_set', eventData: { changes, writer } })`
305
- - Wire endpoint — `POST /v2/runs/:runId/attributes` removed; the same `changes` shape posts to `POST /v2/runs/:id/events`
317
+ - Wire endpoint — `POST /v2/runs/:runId/attributes` is deprecated but temporarily retained for older clients; new calls post the same `changes` shape to events as `attr_set`
306
318
  - Pre-existing attribute values on MVP-era runs remain on the run entity but are not represented in the event log
307
319
 
308
320
  Skew protection means workflows started under the MVP will continue to run with the MVP dispatch path on their original deployment. New deployments use the new path. No in-place data migration is needed.
@@ -157,15 +157,35 @@ To prevent deadlock when polling is executed inline by the step executor, `Run#p
157
157
 
158
158
  ### Mixed Suspensions
159
159
 
160
- A suspension may contain steps, hooks, and waits simultaneously. The handler creates events for all, then chooses between inline execution and queue dispatch:
160
+ A suspension may contain steps, hooks, and waits simultaneously. The handler creates events for all, then dispatches everything we are not running inline as a single parallel batch of queue messages:
161
161
 
162
- - **Steps only** (no waits): one owned step is executed inline; the rest are queued. The loop continues after the inline step completes.
163
- - **Steps + at least one wait**: every step is queued (no inline execution). The handler returns with the wait timeout. Whichever lands first — a step's continuation or the wait timer — drives the next replay.
164
- - **Hooks / waits only**: handler returns with the wait timeout (or no timeout, for hook-only suspensions). The next continuation is driven by external resume or the wait timer.
162
+ ```
163
+ ownedPendingSteps = pendingSteps.filter(owned by this handler)
164
+ inlineStep = ownedPendingSteps[0] // optional
165
+
166
+ dispatches = [
167
+ ...for each non-inline pendingStep: queue stepId message (idempotency=correlationId),
168
+ ...if soonest pending wait: queue delayed continuation
169
+ (delaySeconds=min(remaining, maxDelay),
170
+ idempotency=waitCorrelationId[:hop|:secondBucket]),
171
+ ]
172
+ await Promise.all(dispatches)
173
+
174
+ if (!inlineStep) return
175
+ await executeStep(inlineStep)
176
+ ```
177
+
178
+ The wait timer is queued as its own continuation rather than encoded in the handler's return value (`{ timeoutSeconds }`). This is what makes `Promise.race(step, sleep)` behave correctly: even when the inline step blocks the handler for the full step duration, the wait continuation fires in a separate function invocation. If the sleep wins, that parallel invocation observes `wait_completed` via the "complete elapsed waits" pass and finishes the run; if the step wins, the wait continuation fires later and no-ops on the terminal run via the existing terminal-event check.
179
+
180
+ Step queueing remains unconditional (covers crash recovery: if a prior handler wrote `step_created` but crashed before queueing, a later handler will queue it; idempotency keys dedupe redundant queues across concurrent handlers).
181
+
182
+ Wait continuations are likewise deduplicated, keyed on the wait's correlation ID: while a wait is pending, every replay pass over the run re-observes it and would otherwise enqueue another delayed continuation. A key is attached in all cases, since some worlds serialize key-less workflow messages per run, which would park the continuation behind the handler's own inline step execution.
183
+
184
+ Two situations deliver a continuation while its wait is still pending, and each varies the key so the re-enqueue isn't dropped by a world's dedupe window (which outlives the first delivery): waits longer than the maximum queue delay (23h, bounded by VQS's 24h message retention) are clamped and chained across hops, with the hop index suffixed to the key so each hop dedupes within its window but the chain always advances; and near-elapsed waits (≤2s remaining) use a second-bucketed key suffix so a continuation delivered marginally early (clock skew) can enqueue a fresh short-delay retry. See `runtime/wait-continuation.ts` for the full selection logic.
165
185
 
166
- The "no inline when there's a wait" carve-out is necessary to preserve `Promise.race(step, sleep)` semantics. Inline `await executeStep(...)` blocks the handler for the full step duration, and `wait_completed` events are only created on the *next* loop iteration's "complete elapsed waits" pass — so a longer-running step would always swallow the shorter sleep and `Promise.race` would resolve incorrectly. Queueing the step in this case lets the wait timer drive a continuation in parallel.
186
+ The retry/throttle and hook-conflict paths still return `{ timeoutSeconds }` since their semantics are "redeliver THIS message after a delay" rather than "schedule a fresh wait timer." Those can be unified in a follow-up.
167
187
 
168
- Pure step suspensions (without waits) still benefit from inline execution; the carve-out only costs an extra queue roundtrip when a step and a sleep coexist.
188
+ The unified dispatch requires `world-local` to honor `delaySeconds` on the queue (added in the same PR series). Without it, the wait continuation would fire instantly in dev and trigger a spurious replay before the wait elapsed (recoverable via redelivery, but inefficient and observable as duplicate `step_started` events under contention).
169
189
 
170
190
  ### VM Sandboxing
171
191
 
@@ -43,6 +43,9 @@ Fix common mistakes when creating and executing workflows in the **Workflow SDK*
43
43
  <Card href="/docs/errors/step-not-registered" title="step-not-registered">
44
44
  Resolve step not registered errors caused by deployment mismatches.
45
45
  </Card>
46
+ <Card href="/docs/errors/step-executed-multiple-times" title="Step executed multiple times">
47
+ Diagnose duplicate step_started events from function crashes, timeouts, or OOMs.
48
+ </Card>
46
49
  <Card href="/docs/errors/workflow-not-registered" title="workflow-not-registered">
47
50
  Resolve workflow not registered errors caused by deployment mismatches.
48
51
  </Card>
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: Step executed multiple times
3
+ description: A step ran more than once because its function invocation crashed before it could report a result.
4
+ type: troubleshooting
5
+ summary: Diagnose duplicate step_started events caused by function timeouts, OOMs, or network issues.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ related:
9
+ - /docs/observability
10
+ - /docs/foundations/errors-and-retries
11
+ ---
12
+
13
+ There may be cases where you see multiple `step_started` events for the same step in a workflow run. This happens if the function invocation executing the step crashes unexpectedly, and the step can not report the error. The step will be re-tried according to your retry policy in this case, but no error will be visible in the [Observability UI](/docs/observability).
14
+
15
+ ## Common Causes
16
+
17
+ - **Function timeouts**: if your step code runs longer than the configured maximum function duration, it will be killed. Compare the gap between the `step_started` events to your configured function duration to be sure.
18
+ - **Out of memory (OOM)**: if your step code loads enough data into memory, especially if the step is invoked concurrently, the function invocation might run out of memory. You can see your function's peak memory use by going to the [Observability Query page](https://vercel.com/docs/observability) and showing the **Function Invocation Peak Memory** metric, then filtering down the **Route** to `/.well-known/workflow` endpoints.
19
+ - **Network issues**: persistent firewall, network stability, and related issues might prevent your function from reporting results or errors. This should be temporary.
20
+
21
+ ## Getting Help
22
+
23
+ If you consistently see multiple `step_started` events and have ruled out function timeouts, OOMs, and firewall issues, please [contact support](https://vercel.com/help).
@@ -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 [Idempotency](/docs/foundations/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.
@@ -584,7 +584,7 @@ Stream errors don't trigger automatic retries for the producer step. Design your
584
584
  - [`sleep()` API Reference](/docs/api-reference/workflow/sleep) - Pause workflow execution for a duration
585
585
  - [`start()` API Reference](/docs/api-reference/workflow-api/start) - Start workflows and access the `Run` object
586
586
  - [`getRun()` API Reference](/docs/api-reference/workflow-api/get-run) - Retrieve runs and their streams later
587
- - [world.streams](/docs/api-reference/workflow-api/world/streams) - Low-level stream read/write/close via World SDK
587
+ - [world.streams](/docs/api-reference/workflow-runtime/world/streams) - Low-level stream read/write/close via World SDK
588
588
  - [WorkflowAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI agents with durable, resumable streaming support
589
589
  - [Errors and Retries](/docs/foundations/errors-and-retries) - Understanding error handling and retry behavior
590
590
  - [Serialization](/docs/foundations/serialization) - Understanding what data types can be passed in workflows
@@ -94,7 +94,7 @@ npx workflow cancel \
94
94
  --backend vercel
95
95
  ```
96
96
 
97
- The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-api/world/observability) when you need display-friendly names.
97
+ The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-observability/parse-workflow-name) when you need display-friendly names.
98
98
 
99
99
  In the [observability UI](/docs/observability), use **Rerun on latest** to enqueue the workflow again with the same inputs against the latest deployment.
100
100
 
@@ -105,8 +105,8 @@ To add encryption support to a custom `World`:
105
105
  import type { WorkflowRun, World } from "@workflow/world";
106
106
 
107
107
  export const getEncryptionKeyForRun: World["getEncryptionKeyForRun"] = async (
108
- run,
109
- context
108
+ run: WorkflowRun | string,
109
+ context?: Record<string, unknown>
110
110
  ) => {
111
111
  const runId = typeof run === "string" ? run : run.runId;
112
112
  const deploymentId =
@@ -127,7 +127,7 @@ flowchart TD
127
127
 
128
128
  Unlike other entities, hooks don't have a `status` field—the states above are conceptual. An "active" hook is one that exists in storage, while "disposed" means the hook has been deleted. When a `hook_disposed` event is created, the hook record is removed rather than updated.
129
129
 
130
- While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. Current worlds include the token and the run ID that currently owns it, though older persisted events or world implementations may only include the token. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
130
+ While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. Current worlds include the token and the run ID that currently owns it, though older persisted events or world implementations may only include the token. This causes `hook.getConflict()` to resolve with the conflicting run and the hook's payload promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
131
131
 
132
132
  When a hook is disposed (either explicitly or when its workflow completes), the token is released and can be claimed by future workflows. Hooks are automatically disposed when a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`). The `hook_disposed` event is only needed for explicit disposal before workflow completion.
133
133
 
@@ -188,7 +188,7 @@ Events are categorized by the entity type they affect. Each event contains metad
188
188
  | Event | Description |
189
189
  |-------|-------------|
190
190
  | `hook_created` | Creates a new hook in `active` state. Contains the hook token and optional metadata. |
191
- | `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. Contains the token and, for current worlds, the active hook owner's run ID. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
191
+ | `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. Contains the token and, for current worlds, the active hook owner's run ID. The hook is not created: `hook.getConflict()` resolves with the conflicting run, and awaiting the hook payload rejects with a `HookConflictError`. |
192
192
  | `hook_received` | Records that a payload was delivered to the hook. The hook remains `active` and can receive more payloads. |
193
193
  | `hook_disposed` | Deletes the hook from storage (conceptually transitioning to `disposed` state). The token is released for reuse by future workflows. |
194
194
 
@@ -18,6 +18,15 @@ This feature is experimental and may change before the stable attributes API is
18
18
  [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) attaches plaintext string metadata to the current workflow run. These attributes are displayed in observability CLI/UI.
19
19
  In the future, you'll be able to search and filter runs by attributes.
20
20
 
21
+ You can also seed any attributes directly when starting a run:
22
+
23
+ {/* @skip-typecheck: abbreviated usage; orderWorkflow is defined below */}
24
+ ```typescript lineNumbers
25
+ const run = await start(orderWorkflow, ["ord_123"], {
26
+ attributes: { source: "checkout" }, // [!code highlight]
27
+ })
28
+ ```
29
+
21
30
  ```typescript lineNumbers
22
31
  import { experimental_setAttributes } from "workflow"
23
32
 
@@ -57,10 +66,8 @@ Attribute keys must be 1-256 characters, values must be strings up to 256 bytes,
57
66
 
58
67
  While attributes are experimental:
59
68
 
60
- - Worlds that do not support attributes log a warning and ignore the call.
61
- - Workflow-body storage errors are logged after retries, but do not fail the workflow run.
69
+ - Attributes require a World implementing spec version 4 or later.
70
+ - Writes from workflow and step bodies append native `attr_set` events and immediately materialize `run.attributes`.
71
+ - Storage errors surface rather than being silently ignored: transient errors on workflow-body writes are retried, and a write the World rejects as invalid (for example, exceeding the per-run attribute cap across multiple calls) fails the run with the validation error.
62
72
  - Step-body storage errors throw from `experimental_setAttributes` like any other step-side network write. Catch the error inside the step if the attribute is best-effort.
63
- - Setting attributes from a workflow body is currently slower than the final API will be, because each write goes through an internal workflow step. Step-body calls post directly to the World. Prefer batching related attributes in one call.
64
73
  - Reading and querying attributes is not available yet. A query API is planned.
65
-
66
- In a future release, using attributes with a World that does not support them, or when the World fails to store them, will fail with a [world error](/docs/api-reference/workflow-errors/workflow-world-error). This can be caught and handled to prevent failing a run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "5.0.0-beta.13",
3
+ "version": "5.0.0-beta.15",
4
4
  "description": "Workflow SDK - Build durable, resilient, and observable workflows",
5
5
  "main": "dist/typescript-plugin.cjs",
6
6
  "type": "module",
@@ -57,18 +57,18 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "ms": "2.1.3",
60
- "@workflow/astro": "5.0.0-beta.13",
61
- "@workflow/cli": "5.0.0-beta.13",
62
- "@workflow/core": "5.0.0-beta.13",
60
+ "@workflow/astro": "5.0.0-beta.15",
61
+ "@workflow/cli": "5.0.0-beta.15",
62
+ "@workflow/core": "5.0.0-beta.15",
63
63
  "@workflow/errors": "5.0.0-beta.7",
64
64
  "@workflow/typescript-plugin": "5.0.0-beta.4",
65
65
  "@workflow/utils": "5.0.0-beta.3",
66
- "@workflow/next": "5.0.0-beta.13",
67
- "@workflow/nest": "5.0.0-beta.13",
68
- "@workflow/nitro": "5.0.0-beta.13",
69
- "@workflow/nuxt": "5.0.0-beta.13",
70
- "@workflow/sveltekit": "5.0.0-beta.13",
71
- "@workflow/rollup": "5.0.0-beta.13"
66
+ "@workflow/next": "5.0.0-beta.15",
67
+ "@workflow/nest": "5.0.0-beta.15",
68
+ "@workflow/nitro": "5.0.0-beta.15",
69
+ "@workflow/nuxt": "5.0.0-beta.15",
70
+ "@workflow/sveltekit": "5.0.0-beta.15",
71
+ "@workflow/rollup": "5.0.0-beta.15"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/ms": "2.1.0",
@@ -1,4 +0,0 @@
1
- {
2
- "title": "World SDK",
3
- "pages": ["storage", "streams", "queue", "observability"]
4
- }
@@ -1,164 +0,0 @@
1
- ---
2
- title: Observability Utilities
3
- description: Hydrate step I/O, parse display names, and decrypt workflow data using workflow/observability.
4
- type: reference
5
- summary: "Functions: hydrateResourceIO(), parseStepName(), parseWorkflowName(), parseClassName(), getEncryptionKeyForRun(), hydrateResourceIOWithKey()."
6
- prerequisites:
7
- - /docs/api-reference/workflow-api/get-world
8
- related:
9
- - /docs/api-reference/workflow-api/world/storage
10
- keywords:
11
- - workflow/observability
12
- - hydrateResourceIO
13
- - observabilityRevivers
14
- - parseStepName
15
- - parseWorkflowName
16
- - parseClassName
17
- - getEncryptionKeyForRun
18
- - hydrateResourceIOWithKey
19
- - data hydration
20
- - devalue deserialization
21
- - encryption decryption
22
- - display name parsing
23
- ---
24
-
25
- The `workflow/observability` module provides utilities for working with workflow data in observability and debugging tools. It includes functions to hydrate serialized step I/O, parse machine-readable names into display-friendly formats, and decrypt encrypted workflow data.
26
-
27
- ## Import
28
-
29
- ```typescript lineNumbers
30
- import { // [!code highlight]
31
- hydrateResourceIO, // [!code highlight]
32
- observabilityRevivers, // [!code highlight]
33
- parseStepName, // [!code highlight]
34
- parseWorkflowName, // [!code highlight]
35
- parseClassName, // [!code highlight]
36
- } from "workflow/observability"; // [!code highlight]
37
- ```
38
-
39
- ## Data Hydration
40
-
41
- ### hydrateResourceIO()
42
-
43
- Deserialize step or run data that was serialized using the [devalue](https://github.com/Rich-Harris/devalue) format. Required to display step input/output in your UI.
44
-
45
- ```typescript lineNumbers
46
- import { hydrateResourceIO, observabilityRevivers } from "workflow/observability"; // [!code highlight]
47
-
48
- const step = await world.steps.get(runId, stepId);
49
- const hydrated = hydrateResourceIO(step, observabilityRevivers); // [!code highlight]
50
- console.log(hydrated.input, hydrated.output);
51
- ```
52
-
53
- **Parameters:**
54
-
55
- | Parameter | Type | Description |
56
- |-----------|------|-------------|
57
- | `resource` | `Step \| WorkflowRun` | The step or run with serialized data |
58
- | `revivers` | `Revivers` | Reviver functions for deserialization. Use `observabilityRevivers` for standard use. |
59
-
60
- **Returns:** The resource with hydrated `input` and `output` fields.
61
-
62
- ### observabilityRevivers
63
-
64
- A set of reviver functions that handle standard workflow serialization types (Date, Map, Set, Error, etc.).
65
-
66
- ## Name Parsing
67
-
68
- Workflow and step names are stored as machine-readable identifiers. These utilities extract display-friendly names. All return `{ shortName: string, moduleSpecifier: string } | null`.
69
-
70
- ### parseStepName()
71
-
72
- ```typescript lineNumbers
73
- import { parseStepName } from "workflow/observability"; // [!code highlight]
74
-
75
- const parsed = parseStepName("step//./src/workflows/order//processPayment"); // [!code highlight]
76
- // parsed?.shortName → "processPayment"
77
- // parsed?.moduleSpecifier → "./src/workflows/order"
78
- ```
79
-
80
- ### parseWorkflowName()
81
-
82
- ```typescript lineNumbers
83
- import { parseWorkflowName } from "workflow/observability"; // [!code highlight]
84
-
85
- const parsed = parseWorkflowName("workflow//./src/workflows/order//processOrder"); // [!code highlight]
86
- // parsed?.shortName → "processOrder"
87
- ```
88
-
89
- ### parseClassName()
90
-
91
- ```typescript lineNumbers
92
- import { parseClassName } from "workflow/observability"; // [!code highlight]
93
-
94
- const parsed = parseClassName("class//./src/models//User"); // [!code highlight]
95
- // parsed?.shortName → "User"
96
- ```
97
-
98
- ## Encryption
99
-
100
- For workflows with encrypted step data, decrypt before hydrating.
101
-
102
- ### getEncryptionKeyForRun()
103
-
104
- Retrieve the encryption key used for a specific workflow run.
105
-
106
- {/* @expect-error:2305 */}
107
- ```typescript lineNumbers
108
- import { getEncryptionKeyForRun } from "workflow/observability"; // [!code highlight]
109
-
110
- const key = await getEncryptionKeyForRun(runId); // [!code highlight]
111
- ```
112
-
113
- **Parameters:**
114
-
115
- | Parameter | Type | Description |
116
- |-----------|------|-------------|
117
- | `runId` | `string` | The workflow run ID |
118
-
119
- **Returns:** Encryption key for the run
120
-
121
- ### hydrateResourceIOWithKey()
122
-
123
- Hydrate step or run data using a decryption key. Use this instead of `hydrateResourceIO()` when data is encrypted.
124
-
125
- {/* @expect-error:2305,2724 */}
126
- ```typescript lineNumbers
127
- import { getEncryptionKeyForRun, hydrateResourceIOWithKey } from "workflow/observability"; // [!code highlight]
128
-
129
- const key = await getEncryptionKeyForRun(runId); // [!code highlight]
130
- const hydrated = hydrateResourceIOWithKey(step, key); // [!code highlight]
131
- ```
132
-
133
- **Parameters:**
134
-
135
- | Parameter | Type | Description |
136
- |-----------|------|-------------|
137
- | `resource` | `Step \| WorkflowRun` | The step or run with encrypted serialized data |
138
- | `key` | `EncryptionKey` | The encryption key from `getEncryptionKeyForRun()` |
139
-
140
- **Returns:** The resource with decrypted and hydrated `input` and `output` fields.
141
-
142
- ## Examples
143
-
144
- ### Parse Display Names for a Run's Steps
145
-
146
- ```typescript lineNumbers
147
- import { getWorld } from "workflow/runtime";
148
- import { parseStepName, parseWorkflowName } from "workflow/observability"; // [!code highlight]
149
-
150
- const world = await getWorld();
151
- const run = await world.runs.get(runId, { resolveData: "none" });
152
- console.log("Workflow:", parseWorkflowName(run.workflowName)?.shortName); // [!code highlight]
153
-
154
- const steps = await world.steps.list({ runId, resolveData: "none" });
155
- for (const step of steps.data) {
156
- const parsed = parseStepName(step.stepName); // [!code highlight]
157
- console.log(` ${parsed?.shortName}: ${step.status}`); // [!code highlight]
158
- }
159
- ```
160
-
161
- ## Related
162
-
163
- - [Storage](/docs/api-reference/workflow-api/world/storage) — Query runs, steps, hooks, and events
164
- - [Serialization](/docs/foundations/serialization) — How workflow data is serialized