workflow 5.0.0-beta.15 → 5.0.0-beta.16

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 (32) hide show
  1. package/docs/api-reference/vitest/index.mdx +1 -1
  2. package/docs/api-reference/workflow/create-hook.mdx +7 -1
  3. package/docs/api-reference/workflow/fetch.mdx +5 -0
  4. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
  5. package/docs/api-reference/workflow-api/get-run.mdx +6 -0
  6. package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
  7. package/docs/api-reference/workflow-api/start.mdx +4 -1
  8. package/docs/api-reference/workflow-errors/index.mdx +85 -0
  9. package/docs/api-reference/workflow-runtime/world/storage.mdx +6 -0
  10. package/docs/changelog/index.mdx +1 -1
  11. package/docs/cookbook/advanced/child-workflows.mdx +3 -1
  12. package/docs/cookbook/advanced/publishing-libraries.mdx +13 -12
  13. package/docs/cookbook/advanced/serializable-steps.mdx +3 -3
  14. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +2 -2
  15. package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
  16. package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
  17. package/docs/cookbook/common-patterns/saga.mdx +2 -2
  18. package/docs/cookbook/common-patterns/scheduling.mdx +4 -0
  19. package/docs/cookbook/common-patterns/timeouts.mdx +2 -1
  20. package/docs/cookbook/common-patterns/workflow-composition.mdx +6 -0
  21. package/docs/cookbook/index.mdx +1 -1
  22. package/docs/cookbook/integrations/ai-sdk.mdx +10 -1
  23. package/docs/cookbook/integrations/chat-sdk.mdx +9 -0
  24. package/docs/cookbook/integrations/sandbox.mdx +9 -0
  25. package/docs/errors/step-not-registered.mdx +1 -1
  26. package/docs/foundations/cancellation.mdx +1 -2
  27. package/docs/foundations/hooks.mdx +1 -1
  28. package/docs/foundations/idempotency.mdx +236 -11
  29. package/docs/migration-guides/migrating-from-temporal.mdx +1 -1
  30. package/docs/observability/attributes.mdx +14 -0
  31. package/docs/testing/index.mdx +2 -2
  32. package/package.json +10 -10
@@ -69,7 +69,7 @@ export async function setup() {
69
69
 
70
70
  ### `setupWorkflowTests()`
71
71
 
72
- Sets up an in-process workflow runtime in each test worker. Imports pre-built bundles, creates a [Local World](/docs/worlds/local) instance with direct handlers, and sets it as the global world. Clears all workflow data on each invocation for full test isolation.
72
+ Sets up an in-process workflow runtime in each test worker. Imports pre-built bundles, creates a [Local World](/worlds/local) instance with direct handlers, and sets it as the global world. Clears all workflow data on each invocation for full test isolation.
73
73
 
74
74
  Called automatically by the `workflow()` plugin in `setupFiles`. Use directly only for [manual setup](/docs/testing#manual-setup).
75
75
 
@@ -8,6 +8,7 @@ prerequisites:
8
8
  related:
9
9
  - /docs/api-reference/workflow/define-hook
10
10
  - /docs/api-reference/workflow/create-webhook
11
+ - /docs/foundations/idempotency
11
12
  ---
12
13
 
13
14
  Creates a low-level hook primitive that can be used to resume a workflow run with arbitrary payloads.
@@ -142,7 +143,11 @@ async function processOrder(orderId: string) {
142
143
 
143
144
  Because `createHook()` alone does not suspend the workflow, awaiting `hook.getConflict()` is what actually suspends the run and commits the hook registration. It only waits for registration — to receive payload data from a future `resumeHook()` call, await the hook itself or iterate it with `for await...of`.
144
145
 
145
- On a conflict, the resolved value is a `Run` handle for the run that currently owns the token, with durable step-backed accessors. The duplicate run can decide in code how to handle it: return or log `conflict.runId`, inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` and continue in the current run. See [Idempotency](/docs/foundations/idempotency) for these strategies in context.
146
+ On a conflict, the resolved value is a `Run` handle for the run that currently owns the token, with durable step-backed accessors. The duplicate run can decide in code how to handle it: return or log `conflict.runId`, inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` and continue in the current run. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies in context.
147
+
148
+ <Callout type="info">
149
+ Custom hook tokens are the recommended way to coordinate active workflow runs. Use a deterministic token from your domain, such as an order ID or conversation ID, create the hook near the beginning of the workflow, and check `await hook.getConflict()` before work that depends on owning the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
150
+ </Callout>
146
151
 
147
152
  ### Waiting for Multiple Payloads
148
153
 
@@ -227,3 +232,4 @@ This is equivalent to manually calling `dispose()` but ensures the hook is alway
227
232
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper
228
233
  - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload
229
234
  - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Higher-level HTTP webhook abstraction
235
+ - [Idempotency](/docs/foundations/idempotency) - Deduplicate step side effects and workflow starts
@@ -7,12 +7,17 @@ prerequisites:
7
7
  - /docs/foundations/workflows-and-steps
8
8
  related:
9
9
  - /docs/errors/fetch-in-workflow
10
+ - /docs/foundations/idempotency
10
11
  ---
11
12
 
12
13
  Makes HTTP requests from within a workflow. This is a special step function that wraps the standard `fetch` API, automatically handling serialization and providing retry semantics.
13
14
 
14
15
  This is useful when you need to call external APIs or services from within your workflow.
15
16
 
17
+ <Callout type="warn">
18
+ Because workflow `fetch()` has retry semantics, use idempotency keys when the request mutates an external system, such as creating a charge, sending an email, or enqueueing work. See [Idempotency](/docs/foundations/idempotency).
19
+ </Callout>
20
+
16
21
  <Callout>
17
22
  `fetch` is a *special* type of step function provided and should be called directly inside workflow functions.
18
23
  </Callout>
@@ -5,6 +5,8 @@ type: reference
5
5
  summary: Use getHookByToken to look up a hook's metadata and associated workflow run before resuming it.
6
6
  prerequisites:
7
7
  - /docs/foundations/hooks
8
+ related:
9
+ - /docs/foundations/idempotency
8
10
  ---
9
11
 
10
12
  Retrieves a hook by its unique token, returning the associated workflow run information and any metadata that was set when the hook was created. This function is useful for inspecting hook details before deciding whether to resume a workflow.
@@ -13,6 +15,10 @@ Retrieves a hook by its unique token, returning the associated workflow run info
13
15
  `getHookByToken` is a runtime function that must be called from outside a workflow function.
14
16
  </Callout>
15
17
 
18
+ <Callout type="info">
19
+ Looking up a deterministic hook token is useful in hook-based idempotency flows, but it is only an advisory check. If no hook exists yet, another request can still start the same workflow before your `start()` call registers its hook. Use the lookup to avoid obvious duplicate starts, and handle the race inside the workflow by checking `await hook.getConflict()` before duplicate-sensitive work — on a conflict it resolves with the run that owns the token, so the duplicate can route the caller to the active owner. If duplicates must be rejected before a workflow body runs, keep a durable request record until native atomic start-and-hook registration exists. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
20
+ </Callout>
21
+
16
22
  ```typescript lineNumbers
17
23
  import { getHookByToken } from "workflow/api";
18
24
 
@@ -178,3 +184,4 @@ export async function POST(request: Request) {
178
184
  - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload.
179
185
  - [`createHook()`](/docs/api-reference/workflow/create-hook) - Create a hook in a workflow.
180
186
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper.
187
+ - [Idempotency](/docs/foundations/idempotency) - Deduplicate step side effects and workflow starts.
@@ -5,12 +5,18 @@ type: reference
5
5
  summary: Use getRun to check a workflow run's status and metadata without blocking on completion.
6
6
  prerequisites:
7
7
  - /docs/foundations/starting-workflows
8
+ related:
9
+ - /docs/foundations/idempotency
8
10
  ---
9
11
 
10
12
  Retrieves the workflow run metadata and status information for a given run ID. This function provides immediate access to workflow run details without waiting for completion, making it ideal for status checking and monitoring.
11
13
 
12
14
  Use this function when you need to check workflow status, get timing information, or access workflow metadata without blocking on workflow completion.
13
15
 
16
+ <Callout type="info">
17
+ `getRun()` retrieves a run when you already have its `runId`. It does not look up runs by a business key. For retried requests that should route to one active workflow, use a deterministic hook token and [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token). After a hook conflict, `HookConflictError.conflictingRunId` can be passed to `getRun()` to inspect, stream, or return the active owner. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
18
+ </Callout>
19
+
14
20
  ```typescript lineNumbers
15
21
  import { getRun } from "workflow/api";
16
22
 
@@ -7,6 +7,7 @@ prerequisites:
7
7
  - /docs/foundations/hooks
8
8
  related:
9
9
  - /docs/api-reference/workflow-api/resume-webhook
10
+ - /docs/foundations/idempotency
10
11
  ---
11
12
 
12
13
  Resumes a workflow run by sending a payload to a hook identified by its token.
@@ -155,8 +156,64 @@ export async function POST(request: Request) {
155
156
  }
156
157
  ```
157
158
 
159
+ ### Resume or Start
160
+
161
+ A common endpoint shape is "resume or start": one route that resumes the active workflow run for a business key if one exists, or starts a new run otherwise. This comes up when the workflow uses a deterministic hook token as its idempotency key — for example, one active run per order or conversation.
162
+
163
+ `resumeHook()` is the resume half of that flow. Try it first; if it throws `HookNotFoundError`, no active run owns the token yet, so start the workflow. One subtlety: `start()` returns before the new run executes and registers its hook, so you cannot resume immediately after starting. Retry the resume until the hook is registered — if you drop the payload and only start the workflow, the data from this request is lost.
164
+
165
+ ```typescript lineNumbers
166
+ import { resumeHook, start } from "workflow/api";
167
+ import { HookNotFoundError } from "workflow/errors";
168
+ import { processOrder } from "./workflows/process-order";
169
+
170
+ type OrderRequest = { confirmed: boolean };
171
+
172
+ async function resumeWithRetry(token: string, payload: OrderRequest) {
173
+ for (let attempt = 0; attempt < 5; attempt++) {
174
+ try {
175
+ return await resumeHook(token, payload); // [!code highlight]
176
+ } catch (error) {
177
+ if (!HookNotFoundError.is(error)) throw error;
178
+ await new Promise((resolve) => setTimeout(resolve, 100));
179
+ }
180
+ }
181
+
182
+ throw new Error("Workflow did not register its hook in time");
183
+ }
184
+
185
+ export async function POST(request: Request) {
186
+ const { orderId, confirmed } = await request.json();
187
+ const token = `order:${orderId}`;
188
+ const payload = { confirmed };
189
+
190
+ try {
191
+ // An active run already owns this token: resume it.
192
+ const hook = await resumeHook(token, payload); // [!code highlight]
193
+ return Response.json({ runId: hook.runId, reused: true });
194
+ } catch (error) {
195
+ if (!HookNotFoundError.is(error)) throw error;
196
+ }
197
+
198
+ // No hook yet: start a new run, then retry the resume so this
199
+ // request's payload still reaches the workflow.
200
+ const run = await start(processOrder, [orderId]); // [!code highlight]
201
+ const resumed = await resumeWithRetry(token, payload);
202
+
203
+ // A concurrent request can win the race between `start()` and hook
204
+ // registration; the resume always reaches the actual active owner.
205
+ return Response.json({
206
+ runId: resumed.runId,
207
+ reused: resumed.runId !== run.runId,
208
+ });
209
+ }
210
+ ```
211
+
212
+ See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for the full pattern, including how the workflow claims the token with `hook.getConflict()` and how concurrent starts converge on one active owner.
213
+
158
214
  ## Related Functions
159
215
 
160
216
  - [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) - Get hook details before resuming.
161
217
  - [`createHook()`](/docs/api-reference/workflow/create-hook) - Create a hook in a workflow.
162
218
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper.
219
+ - [Idempotency](/docs/foundations/idempotency) - Deduplicate step side effects and workflow starts.
@@ -5,6 +5,8 @@ type: reference
5
5
  summary: Use start to programmatically enqueue a new workflow run.
6
6
  prerequisites:
7
7
  - /docs/foundations/starting-workflows
8
+ related:
9
+ - /docs/foundations/idempotency
8
10
  ---
9
11
 
10
12
  Start/enqueue a new workflow run.
@@ -54,9 +56,10 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-
54
56
  * In v5, `start()` can also be called directly from a workflow function to spawn a child run or continue work in a new run. See [Workflow Composition](/cookbook/common-patterns/workflow-composition) and [Versioning](/docs/foundations/versioning).
55
57
  * This is different from calling workflow functions directly, which is the typical pattern in Next.js applications.
56
58
  * The function returns immediately after enqueuing the workflow - it doesn't wait for the workflow to complete.
59
+ * Each call to `start()` creates a new workflow run. If retried requests must route to one active workflow, have the workflow create a deterministic hook token and use [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) to reuse an already-registered active hook. The lookup is not atomic with `start()`, so concurrent callers can still create extra runs before the hook is registered; handle that race inside the workflow by checking `await hook.getConflict()` before duplicate-sensitive work — on a conflict it resolves with the run that owns the token, so the duplicate can return the active owner to the caller. If duplicates must be rejected before a workflow body runs, keep a durable request record until native atomic start-and-hook registration exists. See [Idempotency](/docs/foundations/idempotency#run-idempotency).
57
60
  * All arguments must be [serializable](/docs/foundations/serialization).
58
61
  * When `deploymentId` is provided, the argument types and return type become `unknown` since there is no guarantee the workflow function's types will be consistent across different deployments.
59
- * `attributes` seeds plaintext run metadata as part of creation and requires a World implementing spec version 4 or later.
62
+ * `attributes` seeds plaintext run metadata as part of creation and requires a World implementing spec version 4 or later. Keys that start with `$` are reserved for framework and library code; framework-level callers can pass `allowReservedAttributes: true` to seed reserved keys, with the same semantics as the [`experimental_setAttributes`](/docs/api-reference/workflow/experimental-set-attributes) option of the same name.
60
63
 
61
64
  <Callout type="info">
62
65
  If `start()` throws `'start' received an invalid workflow function. Ensure the Workflow Development Kit is configured correctly and the function includes a 'use workflow' directive.`, the passed function was not transformed as a workflow. The two most common causes are a missing `"use workflow"` directive or missing framework integration. See [start-invalid-workflow-function](/docs/errors/start-invalid-workflow-function).
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: "workflow/errors"
3
+ description: Semantic error types thrown by the Workflow SDK and its storage backends.
4
+ type: overview
5
+ summary: Explore the error classes exported from workflow/errors for handling workflow failures.
6
+ related:
7
+ - /docs/foundations/errors-and-retries
8
+ ---
9
+
10
+ API reference for the error classes exported from the `workflow/errors` package.
11
+
12
+ All errors extend [`WorkflowError`](/docs/api-reference/workflow-errors/workflow-error), so you can catch any SDK error with a single `instanceof` check, or narrow to a specific class for fine-grained handling.
13
+
14
+ ## Base Classes
15
+
16
+ <Cards>
17
+ <Card href="/docs/api-reference/workflow-errors/workflow-error" title="WorkflowError">
18
+ Base class for all workflow error types.
19
+ </Card>
20
+ <Card href="/docs/api-reference/workflow-errors/workflow-world-error" title="WorkflowWorldError">
21
+ Base error for failures from workflow storage backends.
22
+ </Card>
23
+ </Cards>
24
+
25
+ ## Registration Errors
26
+
27
+ <Cards>
28
+ <Card href="/docs/api-reference/workflow-errors/workflow-not-registered-error" title="WorkflowNotRegisteredError">
29
+ Thrown when a workflow function is not registered in the current deployment.
30
+ </Card>
31
+ <Card href="/docs/api-reference/workflow-errors/step-not-registered-error" title="StepNotRegisteredError">
32
+ Thrown when a step function is not registered in the current deployment.
33
+ </Card>
34
+ </Cards>
35
+
36
+ ## Run Errors
37
+
38
+ <Cards>
39
+ <Card href="/docs/api-reference/workflow-errors/workflow-run-not-found-error" title="WorkflowRunNotFoundError">
40
+ Thrown when operating on a workflow run that does not exist.
41
+ </Card>
42
+ <Card href="/docs/api-reference/workflow-errors/workflow-run-failed-error" title="WorkflowRunFailedError">
43
+ Thrown when awaiting the return value of a failed workflow run.
44
+ </Card>
45
+ <Card href="/docs/api-reference/workflow-errors/workflow-run-cancelled-error" title="WorkflowRunCancelledError">
46
+ Thrown when awaiting the return value of a cancelled workflow run.
47
+ </Card>
48
+ <Card href="/docs/api-reference/workflow-errors/workflow-run-not-completed-error" title="WorkflowRunNotCompletedError">
49
+ Thrown when requesting the result of a workflow run that has not completed yet.
50
+ </Card>
51
+ <Card href="/docs/api-reference/workflow-errors/workflow-runtime-error" title="WorkflowRuntimeError">
52
+ Thrown when the workflow runtime encounters an execution error, such as serialization failures or timeouts.
53
+ </Card>
54
+ <Card href="/docs/api-reference/workflow-errors/run-expired-error" title="RunExpiredError">
55
+ Thrown when a workflow run has expired and can no longer be operated on.
56
+ </Card>
57
+ <Card href="/docs/api-reference/workflow-errors/run-not-supported-error" title="RunNotSupportedError">
58
+ Thrown when a workflow run requires a newer workflow spec version than the installed SDK supports.
59
+ </Card>
60
+ </Cards>
61
+
62
+ ## Hook Errors
63
+
64
+ <Cards>
65
+ <Card href="/docs/api-reference/workflow-errors/hook-not-found-error" title="HookNotFoundError">
66
+ Thrown when resuming a hook that does not exist.
67
+ </Card>
68
+ <Card href="/docs/api-reference/workflow-errors/hook-conflict-error" title="HookConflictError">
69
+ Thrown when creating a hook with a token that is already in use by another workflow run.
70
+ </Card>
71
+ </Cards>
72
+
73
+ ## Backend Errors
74
+
75
+ <Cards>
76
+ <Card href="/docs/api-reference/workflow-errors/throttle-error" title="ThrottleError">
77
+ Thrown when a request is rate-limited by the workflow backend.
78
+ </Card>
79
+ <Card href="/docs/api-reference/workflow-errors/entity-conflict-error" title="EntityConflictError">
80
+ Thrown when a storage operation conflicts with the current entity state.
81
+ </Card>
82
+ <Card href="/docs/api-reference/workflow-errors/too-early-error" title="TooEarlyError">
83
+ Thrown when a request is made before the system is ready to process it.
84
+ </Card>
85
+ </Cards>
@@ -259,6 +259,12 @@ const hook = await world.hooks.get(hookId); // [!code highlight]
259
259
 
260
260
  Look up a hook by its token. Useful in webhook resume flows where you receive a token in the callback URL.
261
261
 
262
+ <Callout type="info">
263
+ For runtime application code, prefer [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token). Use `world.hooks.getByToken()` when you are working directly with the World storage interface for custom tooling, admin views, or low-level integrations.
264
+
265
+ Hook-token lookup is the low-level form of the recommended idempotency flow: if a hook is already registered for your business key, reuse the hook's `runId` or resume that hook instead of starting another run. If no hook exists yet, start a workflow that creates the deterministic hook near the beginning and checks `await hook.getConflict()` to detect whether another run claimed the token first — on a conflict it resolves with the run that owns the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
266
+ </Callout>
267
+
262
268
  ```typescript lineNumbers
263
269
  const hook = await world.hooks.getByToken(token); // [!code highlight]
264
270
  ```
@@ -13,4 +13,4 @@ Stay up to date with the latest changes to Workflow SDK.
13
13
  ## 2026
14
14
 
15
15
  - [Eager processing of steps and incremental event replay](/docs/changelog/eager-processing) - March 2026
16
- - [Serializable AbortController and AbortSignal](/docs/changelog/serializable-abort-controller) — March 12, 2026
16
+ - Serializable AbortController and AbortSignal — March 12, 2026
@@ -3,6 +3,8 @@ title: Child Workflows
3
3
  description: Spawn child workflows from a parent and wait for completion via hook resume.
4
4
  type: guide
5
5
  summary: Orchestrate independent child workflows from a parent using start(), defineHook(), and startAndWait() — the child resumes the parent's hook when done instead of polling getRun().status.
6
+ related:
7
+ - /docs/api-reference/workflow-api/start
6
8
  ---
7
9
 
8
10
  Use child workflows when a single workflow needs to orchestrate many independent units of work. Each child runs as its own workflow with a separate event log, retry boundary, and failure scope -- if one child fails, it doesn't take down the parent or siblings.
@@ -145,7 +147,7 @@ Polling with `getRun().status` in a `sleep()` loop works, but hook resume is pre
145
147
  - **Zero compute while waiting** — the parent suspends on the hook instead of waking every poll interval
146
148
  - **Immediate wake-up** — the parent resumes as soon as the child finishes, not on the next poll tick
147
149
  - **Typed payloads** — the child sends `{ status, value | error }` directly; no separate `returnValue` fetch step
148
- - **No worker-pool pressure** — `Run#returnValue` polling inside steps can hold worker slots while waiting for children (see [Eager Processing](/changelog/eager-processing))
150
+ - **No worker-pool pressure** — `Run#returnValue` polling inside steps can hold worker slots while waiting for children (see [Eager Processing](/docs/changelog/eager-processing))
149
151
 
150
152
  When a parent calls a child workflow inline with `await` (flattened into the same run), the same wrapper and hook handshake still works — pass the token and `await processDocumentWithCompletion(...)` inside `startAndWait()` instead of calling `start()`.
151
153
 
@@ -270,14 +270,14 @@ Declare `workflow` as an **optional** peer so consumers without the runtime aren
270
270
 
271
271
  ### Runtime detection
272
272
 
273
- Wrap a dynamic `import("workflow")` in try/catch. If either the module isn't installed *or* `getWorkflowMetadata()` throws (call site isn't inside a workflow run), fall through to the standalone path.
273
+ Wrap a dynamic `import("workflow")` in try/catch. If either the module isn't installed *or* `getStepMetadata()` throws (call site isn't inside a workflow step), fall through to the standalone path.
274
274
 
275
275
  ```typescript lineNumbers
276
- async function getWorkflowRunId(): Promise<string | null> { // [!code highlight]
276
+ async function getWorkflowStepId(): Promise<string | null> { // [!code highlight]
277
277
  try {
278
278
  const wf = await import("workflow");
279
- const { workflowRunId } = wf.getWorkflowMetadata();
280
- return workflowRunId;
279
+ const { stepId } = wf.getStepMetadata();
280
+ return stepId;
281
281
  } catch {
282
282
  return null;
283
283
  }
@@ -286,13 +286,14 @@ async function getWorkflowRunId(): Promise<string | null> { // [!code highlight]
286
286
 
287
287
  ### A concrete use case: replay-safe idempotency keys
288
288
 
289
- A payments utility that uses the workflow run ID as a Stripe idempotency key when available, and a fresh UUID otherwise:
289
+ A payments utility that uses the current workflow step ID as a Stripe idempotency key when available, and a fresh UUID otherwise:
290
290
 
291
- {/* @skip-typecheck - depends on getWorkflowRunId defined in the previous block */}
292
291
  ```typescript lineNumbers
292
+ declare function getWorkflowStepId(): Promise<string | null>; // @setup (defined in the previous block)
293
+
293
294
  export async function processPayment(amount: number, currency: string) {
294
- const runId = await getWorkflowRunId();
295
- const idempotencyKey = runId ?? crypto.randomUUID(); // [!code highlight]
295
+ const stepId = await getWorkflowStepId();
296
+ const idempotencyKey = stepId ? `payment:${stepId}` : crypto.randomUUID(); // [!code highlight]
296
297
 
297
298
  const res = await fetch("https://api.stripe.com/v1/charges", {
298
299
  method: "POST",
@@ -306,7 +307,7 @@ export async function processPayment(amount: number, currency: string) {
306
307
  }
307
308
  ```
308
309
 
309
- When called from inside a workflow run, the step wrapping this utility gets a stable idempotency key across replays — Stripe dedupes retries for free. When called from a plain Node.js process, it behaves like any other function and a fresh UUID is generated.
310
+ When called from inside a workflow step, the utility gets a stable idempotency key for that step across retries — Stripe dedupes retries for free. When called from a plain Node.js process, it behaves like any other function and a fresh UUID is generated. For more patterns, see [Idempotency](/docs/foundations/idempotency).
310
311
 
311
312
  ### In production
312
313
 
@@ -330,7 +331,7 @@ Before publishing a workflow library:
330
331
 
331
332
  ## Key APIs
332
333
 
333
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
334
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
335
- - [`start`](/docs/api-reference/workflow/start) — starts a workflow run
334
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) — declares the orchestrator function
335
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — marks functions for durable execution
336
+ - [`start`](/docs/api-reference/workflow-api/start) — starts a workflow run
336
337
  - [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — runtime detection and run ID access
@@ -6,7 +6,7 @@ summary: Return a callback from a step to defer construction of a non-owned clas
6
6
  related:
7
7
  - /docs/foundations/serialization
8
8
  - /docs/foundations/serialization#custom-class-serialization
9
- - /docs/api-reference/workflow/use-step
9
+ - /docs/foundations/workflows-and-steps#step-functions
10
10
  ---
11
11
 
12
12
  <Callout>
@@ -141,7 +141,7 @@ async function uploadFile(
141
141
 
142
142
  ## Key APIs
143
143
 
144
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks a function for extraction and serialization
145
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
144
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — marks a function for extraction and serialization
145
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) — declares the orchestrator function
146
146
  - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — accepts a model factory for durable AI agent streaming
147
147
  - [Custom class serialization](/docs/foundations/serialization#custom-class-serialization) — the companion pattern for classes you own (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`)
@@ -247,8 +247,8 @@ const approvalResult = messages
247
247
 
248
248
  ## Key APIs
249
249
 
250
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
251
- - [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
250
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) — declares the orchestrator function
251
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — declares step functions with retries
252
252
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook with schema validation
253
253
  - [`sleep()`](/docs/api-reference/workflow/sleep) — durable timeout for approval expiry
254
254
  - [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream custom data parts from steps
@@ -1,45 +1,25 @@
1
1
  ---
2
2
  title: Idempotency
3
- description: Ensure external side effects happen exactly once, even when steps are retried or workflows are replayed.
3
+ description: Make step retries safe and coordinate duplicate workflow starts with hook tokens.
4
4
  type: guide
5
- summary: Use step IDs as idempotency keys for external APIs like Stripe so that retries and replays don't create duplicate charges.
5
+ summary: Use step IDs for retry-safe external calls, and use deterministic hook tokens when duplicate requests must route to one active workflow.
6
6
  ---
7
7
 
8
- Workflow steps can be retried (on failure) and replayed (on cold start). If a step calls an external API that isn't idempotent, retries could create duplicate charges, send duplicate emails, or double-process records. Use idempotency keys to make these operations safe.
8
+ Use idempotency when a retry or duplicate request should not repeat the underlying work. In Workflow, there are two common patterns: use the step ID for retry-safe external calls, and use hook tokens to coordinate duplicate workflow starts.
9
9
 
10
10
  ## When to use this
11
11
 
12
- - Charging a payment (Stripe, PayPal)
13
- - Sending transactional emails or SMS
14
- - Creating records in external systems where duplicates are harmful
15
- - Any step that has side effects in systems you don't control
12
+ - A step charges a payment, sends an email, enqueues work, or creates an external record.
13
+ - A route may receive duplicate requests that should map to one active workflow run.
16
14
 
17
- ## Pattern: Step ID as idempotency key
15
+ ## Step idempotency
18
16
 
19
17
  Every step has a unique, deterministic `stepId` available via `getStepMetadata()`. Pass this as the idempotency key to external APIs:
20
18
 
21
19
  ```typescript
22
20
  import { getStepMetadata } from "workflow";
23
21
 
24
- declare function createCharge(customerId: string, amount: number): Promise<{ id: string }>; // @setup
25
- declare function sendReceipt(customerId: string, chargeId: string): Promise<void>; // @setup
26
-
27
- export async function chargeCustomer(customerId: string, amount: number) {
28
- "use workflow";
29
-
30
- const charge = await createCharge(customerId, amount);
31
- await sendReceipt(customerId, charge.id);
32
-
33
- return { customerId, chargeId: charge.id, status: "completed" };
34
- }
35
- ```
36
-
37
- ### Step function with idempotency key
38
-
39
- ```typescript
40
- import { getStepMetadata } from "workflow";
41
-
42
- async function createCharge(
22
+ export async function createCharge(
43
23
  customerId: string,
44
24
  amount: number
45
25
  ): Promise<{ id: string }> {
@@ -69,39 +49,43 @@ async function createCharge(
69
49
 
70
50
  return charge.json();
71
51
  }
72
-
73
- async function sendReceipt(customerId: string, chargeId: string): Promise<void> {
74
- "use step";
75
-
76
- const { stepId } = getStepMetadata();
77
-
78
- await fetch("https://api.example.com/receipts", {
79
- method: "POST",
80
- headers: { "Idempotency-Key": stepId },
81
- body: JSON.stringify({ customerId, chargeId }),
82
- });
83
- }
84
52
  ```
85
53
 
86
- ## Race condition caveats
54
+ See [Step Idempotency](/docs/foundations/idempotency#step-idempotency) for why `stepId` is stable across retries and how to think about external API conflicts.
87
55
 
88
- Workflow does not currently provide distributed locking or true exactly-once delivery across concurrent runs. If two workflow runs could process the same entity concurrently:
56
+ ## Run idempotency
89
57
 
90
- - **Rely on the external API's idempotency** (like Stripe's `Idempotency-Key`) rather than checking a local flag.
91
- - **Don't use check-then-act patterns** like "read a flag, then write if not set" -- another run could read the same flag between your read and write.
58
+ For duplicate workflow-start requests, derive a hook token from your domain key. You can avoid obvious duplicate starts by checking whether an active hook already owns that token before calling `start()`:
92
59
 
93
- If your external API doesn't support idempotency keys natively, consider adding a deduplication layer (e.g., a database unique constraint on the operation ID).
60
+ ```typescript
61
+ import { getHookByToken, start } from "workflow/api";
62
+ import { HookNotFoundError } from "workflow/errors";
63
+ import { processOrder } from "./workflows/process-order";
64
+
65
+ export async function POST(request: Request) {
66
+ const { orderId } = await request.json();
67
+ const token = `order:${orderId}`;
68
+
69
+ try {
70
+ const hook = await getHookByToken(token); // [!code highlight]
71
+ return Response.json({ runId: hook.runId, reused: true });
72
+ } catch (error) {
73
+ if (!HookNotFoundError.is(error)) throw error;
74
+ }
94
75
 
95
- ## Tips
76
+ const run = await start(processOrder, [orderId]); // [!code highlight]
77
+ return Response.json({ runId: run.runId, reused: false });
78
+ }
79
+ ```
96
80
 
97
- - **`stepId` is deterministic.** It's the same value across retries and replays of the same step, making it a reliable idempotency key.
98
- - **Always provide idempotency keys for non-idempotent external calls.** Even if you think a step won't be retried, cold-start replay will re-execute it.
99
- - **Handle 409/conflict as success.** If an external API returns "already processed," treat that as a successful result, not an error.
100
- - **Make your own APIs idempotent** where possible. Accept an idempotency key and return the cached result on duplicate requests.
81
+ The workflow should create the deterministic hook and check `await hook.getConflict()` before duplicate-sensitive work — awaiting `getConflict()` suspends the workflow to commit the hook registration and resolves with the conflicting run when another active run already owns the token (or `null` once the hook is registered). See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for the full pattern, including how to steer an active run with `resumeHook()` and how to handle the current race between `start()` and hook registration.
101
82
 
102
83
  ## Key APIs
103
84
 
104
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- declares the orchestrator function
105
- - [`"use step"`](/docs/api-reference/workflow/use-step) -- declares step functions with full Node.js access
106
- - [`getStepMetadata()`](/docs/api-reference/step/get-step-metadata) -- provides the deterministic `stepId` for idempotency keys
85
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) -- declares the orchestrator function
86
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) -- declares step functions with full Node.js access
87
+ - [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) -- provides the deterministic `stepId` for idempotency keys
88
+ - [`createHook()`](/docs/api-reference/workflow/create-hook) -- creates a hook with an optional deterministic token
89
+ - [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) -- finds the active hook for a token
90
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resumes the active hook when the duplicate request carries data
107
91
  - [`start()`](/docs/api-reference/workflow-api/start) -- starts a new workflow run
@@ -224,5 +224,5 @@ export async function downloadWithRetry(url: string) {
224
224
  - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
225
225
  - [`RetryableError`](/docs/api-reference/workflow/retryable-error) -- signals the runtime to retry after a delay
226
226
  - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- signals a permanent failure, skipping retries
227
- - [`getStepMetadata()`](/docs/api-reference/step/get-step-metadata) -- provides the current attempt number and step ID
227
+ - [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) -- provides the current attempt number and step ID
228
228
  - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable pause for circuit breaker cooldowns
@@ -241,7 +241,7 @@ export async function subscriptionUpgradeSaga(accountId: string, seats: number)
241
241
 
242
242
  ## Key APIs
243
243
 
244
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- declares the orchestrator function
245
- - [`"use step"`](/docs/api-reference/workflow/use-step) -- declares step functions with full Node.js access
244
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) -- declares the orchestrator function
245
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) -- declares step functions with full Node.js access
246
246
  - [`FatalError`](/docs/api-reference/workflow/fatal-error) -- non-retryable error that triggers compensation
247
247
  - [`getWritable()`](/docs/api-reference/workflow/get-writable) -- streams data from workflows for real-time UI updates
@@ -106,6 +106,10 @@ export async function POST(req: Request) {
106
106
  3. **Race** — `Promise.race([sleep(...), hook])` blocks until either the timer fires or the hook is resumed, whichever comes first.
107
107
  4. **Fresh hooks per window** — after a sleep completes normally, the previous hook instance is consumed. A new `.create()` call registers a fresh hook for the next sleep window, reusing the same token.
108
108
 
109
+ <Callout type="info">
110
+ Deterministic hook tokens can also serve as the idempotency point for scheduled runs. If duplicate schedule starts would send duplicate campaigns or reminders, create a hook with a token derived from the campaign key near the beginning of the workflow and route retries through that hook. If two scheduled starts race, the duplicate run can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the duplicate can defer to it. See [Idempotency](/docs/foundations/idempotency).
111
+ </Callout>
112
+
109
113
  ## Adapting to your use case
110
114
 
111
115
  - **Change durations** — replace `"2d"` with any duration string (`"1h"`, `"7d"`, `"30m"`) or a `Date` object for absolute times.
@@ -80,7 +80,7 @@ export async function waitForApproval(requestId: string) {
80
80
  4. **Throw to fail the workflow** — inside a workflow function, throwing an `Error` exits the run with that error. Use `FatalError` inside steps; throw plain errors inside workflows.
81
81
 
82
82
  <Callout type="warn">
83
- **The losing operation keeps running.** `Promise.race` doesn't cancel — when the sleep wins, the underlying step (or model call, or HTTP request) continues to completion in the background. This is fine for idempotent reads but matters when the operation has side effects or costs money. Pass an `AbortSignal` into the step to cancel it cooperatively see the [Cancellation Guide](/docs/foundations/cancellation) for patterns.
83
+ **The losing operation keeps running.** `Promise.race` doesn't cancel — when the sleep wins, the underlying step (or model call, or HTTP request) continues to completion in the background. This is fine for idempotent reads but matters when the operation has side effects or costs money. Pass an `AbortSignal` into the step to cancel it cooperatively, and use idempotency keys for non-idempotent side effects. See the [Cancellation Guide](/docs/foundations/cancellation) and [Idempotency](/docs/foundations/idempotency) for patterns.
84
84
  </Callout>
85
85
 
86
86
  ## Adapting to your use case
@@ -96,4 +96,5 @@ export async function waitForApproval(requestId: string) {
96
96
  - [`sleep()`](/docs/api-reference/workflow/sleep) — durable wait (survives restarts, zero compute cost)
97
97
  - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) — create a webhook URL the workflow can race against
98
98
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) — typed hook for in-process cancellation
99
+ - [Idempotency](/docs/foundations/idempotency) — protect side effects that may keep running after a timeout
99
100
  - [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) — race operations against deadlines
@@ -5,6 +5,7 @@ type: guide
5
5
  summary: Compose workflows two ways — direct await flattens the child into the parent's event log, while background spawn via start() runs the child as an independent run.
6
6
  related:
7
7
  - /cookbook/advanced/child-workflows
8
+ - /cookbook/common-patterns/idempotency
8
9
  - /docs/api-reference/workflow-api/start
9
10
  - /docs/api-reference/workflow-api/get-run
10
11
  ---
@@ -77,6 +78,10 @@ export async function processOrder(orderId: string) {
77
78
 
78
79
  The parent continues immediately after `start()` returns. The child runs independently and can be monitored separately using the returned `runId` (e.g., via [`getRun()`](/docs/api-reference/workflow-api/get-run)).
79
80
 
81
+ <Callout type="info">
82
+ Each background spawn creates a separate run. If duplicate requests must route to one active child workflow, have the child create a deterministic hook token from the business key and use that hook as the idempotency point. If concurrent starts race, the losing child can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the child can point callers at it. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
83
+ </Callout>
84
+
80
85
  <Callout type="info">
81
86
  If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. See [Versioning](/docs/foundations/versioning) for the full model. This is currently a Vercel-specific feature, and other Worlds may map the concept to their own deployment runtimes. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
82
87
  </Callout>
@@ -109,3 +114,4 @@ If you want the child workflow to run on the latest deployment rather than the c
109
114
  - [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions with full Node.js access
110
115
  - [`start()`](/docs/api-reference/workflow-api/start) — spawn a child workflow as a separate run
111
116
  - [`getRun()`](/docs/api-reference/workflow-api/get-run) — retrieve a workflow run's status and return value
117
+ - [Idempotency](/docs/foundations/idempotency) — deduplicate step side effects and workflow starts
@@ -21,7 +21,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f
21
21
  - [**Rate Limiting**](/cookbook/common-patterns/rate-limiting) — Handle 429 responses and transient failures with RetryableError and backoff
22
22
  - [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
23
23
  - [**Timeouts**](/cookbook/common-patterns/timeouts) — Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
24
- - [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects happen exactly once, even when steps retry
24
+ - [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects and duplicate starts are safe to retry
25
25
  - [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably
26
26
 
27
27
  ## Integrations
@@ -130,6 +130,10 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
130
130
 
131
131
  One endpoint handles first turn, follow-ups, and the `/done` exit. The client sends `runId` in the body to distinguish first vs follow-up.
132
132
 
133
+ <Callout type="info">
134
+ The first turn calls `start()` and then returns the `runId`. If your client or platform can retry that first request before it receives and stores the `runId`, use an atomic conversation or request key before starting the workflow. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
135
+ </Callout>
136
+
133
137
  ```typescript title="app/api/support/route.ts" lineNumbers
134
138
  import type { UIMessage, UIMessageChunk } from "ai";
135
139
  import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
@@ -350,6 +354,10 @@ In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather
350
354
 
351
355
  Clients can send a `runId` from a long-gone workflow (localStorage, back button, server restart). Wrap the follow-up path in a try/catch for `not found` / `expired` and fall through to the first-turn code path to start a fresh workflow.
352
356
 
357
+ ### Make the first turn idempotent when needed
358
+
359
+ This example stores the `runId` after the first response. For strict one-session-per-thread behavior, use a deterministic hook token derived from the thread ID or conversation ID and route retries through the active hook. See [Idempotency](/docs/foundations/idempotency).
360
+
353
361
  ## streamText vs DurableAgent
354
362
 
355
363
  | | `streamText()` (this pattern) | `DurableAgent` |
@@ -376,8 +384,9 @@ Use `DurableAgent` for most agent use cases. Use `streamText` when you need the
376
384
 
377
385
  **Workflow SDK**
378
386
 
379
- * [`"use step"`](/docs/api-reference/workflow/use-step) — applied to `runTurn` to make each turn a durable, retryable unit
387
+ * [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — applied to `runTurn` to make each turn a durable, retryable unit
380
388
  * [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages
381
389
  * [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output
382
390
  * [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams
383
391
  * [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport) — passes `runId` between turns
392
+ * [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first turns and side effects
@@ -176,6 +176,10 @@ export type ChatTurnPayload = {
176
176
 
177
177
  Handlers live outside the workflow file so adapter dependencies don't leak in. They decide whether to start a new workflow or resume an existing one, then store the `runId` in thread state:
178
178
 
179
+ <Callout type="info">
180
+ If the platform can deliver the same first message concurrently, use a deterministic hook token derived from the thread ID so duplicate handlers route to the active chat session hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
181
+ </Callout>
182
+
179
183
  ```typescript title="lib/chat-session-handlers.ts" lineNumbers
180
184
  import type { Message, Thread } from "chat";
181
185
  import { getRun, resumeHook, start } from "workflow/api";
@@ -289,6 +293,10 @@ Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend
289
293
 
290
294
  A workflow run ends but its `runId` is still cached in thread state. The next message calls `resumeHook` on a dead run and throws `not found` / `expired`. Gate on `getRun(runId).exists` before resuming, or catch the error and fall through to `startSession`. Either way the user's message must not be dropped.
291
295
 
296
+ ### Make first-message routing atomic
297
+
298
+ Thread state is also the idempotency boundary for starting sessions. Back it with a state adapter or database operation that can atomically claim the thread before `startSession()` runs when duplicate sessions would be harmful.
299
+
292
300
  ### Keep the hook outside the loop
293
301
 
294
302
  One `chatTurnHook.create({ token: workflowRunId })` per workflow run, reused every iteration. Creating a new hook with the same token throws `HookConflictError`. This is the same rule as the [AI SDK](/docs/cookbook/integrations/ai-sdk) and [Sandbox](/docs/cookbook/integrations/sandbox) session patterns.
@@ -305,3 +313,4 @@ Slack wants a 200 within 3 seconds. The webhook handler returns immediately afte
305
313
  - [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.exists` before resuming, to detect stale `runId`s.
306
314
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) — per-turn suspension point inside the workflow.
307
315
  - [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat) — makes the bot resolvable from inside step functions.
316
+ - [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first messages and side effects.
@@ -300,6 +300,10 @@ export async function sandboxSessionWorkflow() {
300
300
 
301
301
  Two endpoints. `/start` accepts an optional `{ runId }` — if the run still exists, it replays the event log from index 0 so a returning client fully rehydrates. `/command` resumes the hook and returns immediately; command output lands on the `/start` stream.
302
302
 
303
+ <Callout type="info">
304
+ This example starts a fresh sandbox session when no `runId` is provided. If your product needs one sandbox session per user, project, or task, use a deterministic hook token derived from that session key and route retries through the active hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
305
+ </Callout>
306
+
303
307
  ```typescript title="app/api/sandbox/start/route.ts" lineNumbers
304
308
  import { start, getRun } from "workflow/api";
305
309
  import { sandboxSessionWorkflow } from "@/workflows/sandbox-session";
@@ -505,6 +509,10 @@ Stream closure must happen inside a `"use step"` function. Calling `writable.clo
505
509
 
506
510
  Clients can hold `runId`s from long-gone workflow runs (localStorage, back button, server restart). Gate the reconnect path on `run.exists` and fall through to starting fresh. On `hook.resume`, catch `not found` / `expired` and return 410 so the client clears its state.
507
511
 
512
+ ### Decide whether `/start` should be idempotent
513
+
514
+ The sample treats a missing or stale `runId` as a request for a new session. For one-session-per-resource behavior, use a durable resource key, such as `projectId` or `taskId`, to claim or retrieve the run before starting a new one.
515
+
508
516
  ### Keep the hook outside the loop
509
517
 
510
518
  Each iteration's `hook.then(...)` attaches a listener to the same hook instance. Creating a new hook per iteration with the same token throws `HookConflictError`. One hook, one token (`workflowRunId`), reused every iteration.
@@ -518,3 +526,4 @@ Each iteration's `hook.then(...)` attaches a listener to the same hook instance.
518
526
  - [`sleep()`](/docs/api-reference/workflow/sleep) — durable timer that powers both idle hibernation and proactive refresh
519
527
  - [`getRun()`](/docs/api-reference/workflow-api/get-run) — look up a run and replay its event log for reconnection
520
528
  - [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable NDJSON event stream
529
+ - [Idempotency](/docs/foundations/idempotency) — choose when `/start` should reuse an existing run
@@ -4,7 +4,7 @@ description: A step function is not registered in the current deployment.
4
4
  type: troubleshooting
5
5
  summary: Resolve step not registered errors caused by build issues.
6
6
  prerequisites:
7
- - /docs/foundations/steps
7
+ - /docs/foundations/workflows-and-steps
8
8
  related:
9
9
  - /docs/errors/workflow-not-registered
10
10
  - /docs/api-reference/workflow-errors/step-not-registered-error
@@ -6,7 +6,6 @@ summary: Cancel in-flight work with AbortSignal or stop entire workflow runs.
6
6
  prerequisites:
7
7
  - /docs/foundations/workflows-and-steps
8
8
  related:
9
- - /docs/foundations/common-patterns
10
9
  - /docs/foundations/hooks
11
10
  - /docs/how-it-works/cancellation
12
11
  ---
@@ -455,6 +454,6 @@ This is safe even if both steps have already completed — aborting a finished o
455
454
 
456
455
  - [How Cancellation Works](/docs/how-it-works/cancellation) — Hook and stream backing, serialization internals
457
456
  - [Serialization](/docs/foundations/serialization) — Understanding serializable types
458
- - [Common Patterns](/docs/foundations/common-patterns) — Timeout and race patterns
457
+ - [Cookbook](/v5/cookbook) — Timeout, race, and other reliability patterns
459
458
  - [Hooks](/docs/foundations/hooks) — Pausing workflows for external events
460
459
  - [Errors and Retries](/docs/foundations/errors-and-retries) — Handling step failures
@@ -112,7 +112,7 @@ export async function orderWorkflow(orderId: string) {
112
112
  }
113
113
  ```
114
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.
115
+ Calling `createHook()` on its own does not register the hook — registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the hook registration, then resolves with `null` once the hook is registered and ready to receive payloads, or with a `Run` handle for the run that owns the token if another active hook already claimed it (see [`HookConflictError`](/docs/errors/hook-conflict)). For `hook_conflict` events persisted by older worlds that did not record the owning run's ID, `getConflict()` rejects with `HookConflictError` instead of resolving with an incomplete handle. The conflicting run's accessors are durable steps, so the workflow can inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` — see [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies.
116
116
 
117
117
  ### Custom Tokens for Deterministic Hooks
118
118
 
@@ -1,23 +1,27 @@
1
1
  ---
2
2
  title: Idempotency
3
- description: Ensure operations can be safely retried without producing duplicate side effects.
3
+ description: Make step retries safe and coordinate duplicate workflow starts with hook tokens.
4
4
  type: conceptual
5
- summary: Prevent duplicate side effects when retrying operations in steps.
5
+ summary: Use step IDs for retry-safe external calls, and route duplicate workflow-start requests through deterministic hook tokens.
6
6
  prerequisites:
7
7
  - /docs/foundations/workflows-and-steps
8
8
  related:
9
9
  - /docs/foundations/errors-and-retries
10
+ - /docs/foundations/starting-workflows
11
+ - /docs/foundations/hooks
10
12
  ---
11
13
 
12
- Idempotency is a property of an operation that ensures it can be safely retried without producing duplicate side effects.
14
+ Idempotency is a property of an operation that ensures repeated attempts have the same effect as a single attempt.
15
+
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
+
18
+ ## Step Idempotency
13
19
 
14
20
  In distributed systems (calling external APIs), it is not always possible to ensure an operation has only been performed once just by seeing if it succeeds.
15
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.
16
22
 
17
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.
18
24
 
19
- ## The core pattern: use the step ID as your idempotency key
20
-
21
25
  Every step invocation has a stable `stepId` that stays the same across retries.
22
26
  Use it as the idempotency key when calling third-party APIs.
23
27
 
@@ -27,7 +31,7 @@ import { getStepMetadata } from "workflow";
27
31
  async function chargeUser(userId: string, amount: number) {
28
32
  "use step";
29
33
 
30
- const { stepId } = getStepMetadata();
34
+ const { stepId } = getStepMetadata(); // [!code highlight]
31
35
 
32
36
  // Example: Stripe-style idempotency key
33
37
  // This guarantees only one charge is created even if the step retries
@@ -49,14 +53,235 @@ Why this works:
49
53
  - **Stable across retries**: `stepId` does not change between attempts.
50
54
  - **Globally unique per step**: Fulfills the uniqueness requirement for an idempotency key.
51
55
 
52
- ## Best practices
56
+ ## Run idempotency
57
+
58
+ Step idempotency protects side effects **inside** a workflow run. Run idempotency answers a different question: if the same API request is sent twice, should it create one workflow run or two?
59
+
60
+ Because [hooks](/docs/foundations/hooks) already ensure globally unique active tokens, Workflow can use the same mechanism to coordinate duplicate requests while a run is active.
61
+
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
+
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
+
66
+ ```typescript lineNumbers
67
+ import { createHook } from "workflow";
68
+
69
+ type OrderRequest = { confirmed: boolean };
70
+ type OrderResult =
71
+ | { status: "processed" | "cancelled" }
72
+ | { status: "duplicate"; runId: string };
73
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
74
+
75
+ export async function processOrder(orderId: string): Promise<OrderResult> {
76
+ "use workflow";
77
+
78
+ using request = createHook<OrderRequest>({ // [!code highlight]
79
+ token: `order:${orderId}`, // [!code highlight]
80
+ }); // [!code highlight]
81
+
82
+ const conflict = await request.getConflict(); // [!code highlight]
83
+ if (conflict) { // [!code highlight]
84
+ // Another active run already owns this order's token. // [!code highlight]
85
+ return { status: "duplicate" as const, runId: conflict.runId }; // [!code highlight]
86
+ } // [!code highlight]
87
+
88
+ const { confirmed } = await request;
89
+
90
+ if (!confirmed) {
91
+ return { status: "cancelled" as const };
92
+ }
93
+
94
+ await chargeOrder(orderId);
95
+ return { status: "processed" as const };
96
+ }
97
+ ```
98
+
99
+ The runtime creates the hook atomically. At most one active hook can own `order:${orderId}`, so duplicate workflow runs converge on one active 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
+
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
+
103
+ ```typescript lineNumbers
104
+ import { resumeHook, start } from "workflow/api";
105
+ import { HookNotFoundError } from "workflow/errors";
106
+ import { processOrder } from "./workflows/process-order";
107
+
108
+ type OrderRequest = { confirmed: boolean };
109
+
110
+ async function resumeOrder(token: string, payload: OrderRequest) {
111
+ for (let attempt = 0; attempt < 5; attempt++) {
112
+ try {
113
+ return await resumeHook(token, payload); // [!code highlight]
114
+ } catch (error) {
115
+ if (!HookNotFoundError.is(error)) throw error;
116
+ await new Promise((resolve) => setTimeout(resolve, 100));
117
+ }
118
+ }
119
+
120
+ throw new Error("Order workflow did not register its hook in time");
121
+ }
122
+
123
+ export async function POST(request: Request) {
124
+ const { orderId, confirmed } = await request.json();
125
+ const token = `order:${orderId}`;
126
+ const payload = { confirmed };
127
+
128
+ try {
129
+ const hook = await resumeHook(token, payload); // [!code highlight]
130
+ return Response.json({ runId: hook.runId, reused: true });
131
+ } catch (error) {
132
+ if (!HookNotFoundError.is(error)) throw error;
133
+ }
134
+
135
+ const run = await start(processOrder, [orderId]); // [!code highlight]
136
+ const resumed = await resumeOrder(token, payload);
137
+
138
+ // A concurrent request's run may have won the race between `start()` // [!code highlight]
139
+ // and hook registration. The resume always reaches the actual active // [!code highlight]
140
+ // owner, so compare run IDs instead of waiting for this run to finish. // [!code highlight]
141
+ return Response.json({ // [!code highlight]
142
+ runId: resumed.runId, // [!code highlight]
143
+ reused: resumed.runId !== run.runId, // [!code highlight]
144
+ }); // [!code highlight]
145
+ }
146
+ ```
147
+
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 — 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
+ </Callout>
151
+
152
+ This is active-run coordination. When the workflow completes and disposes the hook, the token can be used again. If a duplicate request after completion must return the original result instead of starting fresh work, persist that completed result under the same domain key.
153
+
154
+ ### Conflict-handling strategies
155
+
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
+
158
+ The example above implements **reject the duplicate**: return the owner's `runId` and let the caller decide. Other common strategies:
159
+
160
+ **Adopt the owner's result.** Wait for the active run to finish and return its result, so callers cannot tell which run did the work:
161
+
162
+ ```typescript lineNumbers
163
+ import { createHook } from "workflow";
164
+
165
+ type OrderRequest = { confirmed: boolean };
166
+ declare function processOwnedOrder(orderId: string): Promise<{ status: string }>; // @setup
167
+
168
+ export async function processOrder(orderId: string) {
169
+ "use workflow";
170
+
171
+ using request = createHook<OrderRequest>({
172
+ token: `order:${orderId}`,
173
+ });
174
+
175
+ const conflict = await request.getConflict();
176
+ if (conflict) {
177
+ // Callers get the same result regardless of which run did the work.
178
+ return await conflict.returnValue; // [!code highlight]
179
+ }
180
+
181
+ return await processOwnedOrder(orderId);
182
+ }
183
+ ```
184
+
185
+ **Inspect the owner before deciding.** Branch on the owner's live state:
186
+
187
+ ```typescript lineNumbers
188
+ import { createHook } from "workflow";
189
+
190
+ type OrderRequest = { confirmed: boolean };
191
+ declare function processOwnedOrder(orderId: string): Promise<{ status: string }>; // @setup
192
+
193
+ export async function processOrder(orderId: string) {
194
+ "use workflow";
195
+
196
+ using request = createHook<OrderRequest>({
197
+ token: `order:${orderId}`,
198
+ });
199
+
200
+ const conflict = await request.getConflict();
201
+ if (conflict) {
202
+ const status = await conflict.status; // [!code highlight]
203
+ if (status === "running") {
204
+ return { status: "duplicate" as const, runId: conflict.runId };
205
+ }
206
+ // Owner already reached a terminal state; its hook will be released.
207
+ }
208
+
209
+ return await processOwnedOrder(orderId);
210
+ }
211
+ ```
212
+
213
+ **Signal the owner instead of doing the work.** The duplicate run knows the token, so it can deliver this run's input to the owner's hook from a step:
214
+
215
+ ```typescript lineNumbers
216
+ import { createHook } from "workflow";
217
+ import { resumeHook } from "workflow/api";
218
+
219
+ type OrderRequest = { confirmed: boolean };
220
+
221
+ async function forwardToOwner(token: string, payload: OrderRequest) {
222
+ "use step";
223
+ await resumeHook(token, payload); // [!code highlight]
224
+ }
225
+
226
+ export async function processOrder(orderId: string, confirmed: boolean) {
227
+ "use workflow";
228
+
229
+ const token = `order:${orderId}`;
230
+ using request = createHook<OrderRequest>({ token });
231
+
232
+ const conflict = await request.getConflict();
233
+ if (conflict) {
234
+ await forwardToOwner(token, { confirmed }); // [!code highlight]
235
+ return { status: "forwarded" as const, runId: conflict.runId };
236
+ }
237
+
238
+ // ... own the token and do the work
239
+ }
240
+ ```
241
+
242
+ **Supersede the owner.** Newest-wins: cancel the active run, then claim the released token. Cancellation disposes the owner's hooks; the retry loop covers the window where that disposal has not propagated yet:
243
+
244
+ ```typescript lineNumbers
245
+ import { createHook } from "workflow";
246
+
247
+ type OrderRequest = { confirmed: boolean };
248
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
249
+
250
+ export async function processOrderNewestWins(orderId: string) {
251
+ "use workflow";
252
+
253
+ const token = `order:${orderId}`;
254
+
255
+ for (let attempt = 0; attempt < 3; attempt++) {
256
+ using request = createHook<OrderRequest>({ token });
257
+
258
+ const conflict = await request.getConflict();
259
+ if (!conflict) {
260
+ // Token claimed — this run is now the owner.
261
+ const { confirmed } = await request;
262
+ if (confirmed) {
263
+ await chargeOrder(orderId);
264
+ }
265
+ return { status: "processed" as const };
266
+ }
267
+
268
+ await conflict.cancel(); // [!code highlight]
269
+ }
270
+
271
+ throw new Error(`Could not claim ${token} after cancelling the owner`);
272
+ }
273
+ ```
274
+
275
+ 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.
53
276
 
54
- - **Always provide idempotency keys to external side effects that are not idempotent** inside steps (payments, emails, SMS, queues).
55
- - **Prefer `stepId` as your key**; it is stable across retries and unique per step.
56
- - **Keep keys deterministic**; avoid including timestamps or attempt counters.
57
- - **Handle 409/conflict responses** gracefully; treat them as success if the prior attempt completed.
277
+ Because this pattern uses hooks for idempotency, duplicate requests can also inject additional data and steer the existing run. The route example above uses `resumeHook()` for that: if the hook already exists, the duplicate request resumes the active workflow; if the hook is not registered yet, the route starts the workflow and retries `resumeHook()` so the payload is not dropped.
58
278
 
59
279
  ## Related docs
60
280
 
61
281
  - Learn about retries in [Errors & Retrying](/docs/foundations/errors-and-retries)
62
282
  - API reference: [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata)
283
+ - API reference: [`createHook()`](/docs/api-reference/workflow/create-hook)
284
+ - API reference: [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token)
285
+ - API reference: [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook)
286
+ - API reference: [`start()`](/docs/api-reference/workflow-api/start)
287
+ - Learn about deterministic hook tokens in [Hooks](/docs/foundations/hooks)
@@ -295,7 +295,7 @@ Remove the Worker process, `@temporalio/*` dependencies, and the Temporal Server
295
295
  - **Event history archival.** Temporal archives histories to S3/GCS for long-term retention. Workflow SDK event logs are durable, but retention depends on the integration you are using. For example, see [Vercel Workflow Storage Retention](https://vercel.com/docs/workflows/pricing#storage-retention) for Vercel.
296
296
  - **Per-activity timeouts (`startToCloseTimeout`, `scheduleToCloseTimeout`, `heartbeatTimeout`).** Implement deadlines inside the step with `AbortSignal.timeout(ms)`, or wrap the call in `Promise.race(step(), sleep(...))` from the workflow.
297
297
  - **Rich retry policy (`initialInterval`, `backoffCoefficient`, `maximumInterval`, `nonRetryableErrorTypes`).** Only `maxRetries` is configurable. Classify retryability with `RetryableError`/`FatalError`; control delay between attempts via `new RetryableError(msg, { retryAfter: '5s' })`.
298
- - **Workers + task queues.** Managed deployments replace workers; self-hosted deployments still need a `World` implementation (see [/docs/deploying/world](/docs/deploying/world)).
298
+ - **Workers + task queues.** Managed deployments replace workers; self-hosted deployments still need a `World` implementation (see [/docs/deploying/building-a-world](/docs/deploying/building-a-world)).
299
299
 
300
300
  ## Quick-start checklist
301
301
 
@@ -62,6 +62,20 @@ export async function cleanupAttributes() {
62
62
 
63
63
  Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with `$` are reserved for framework and library code.
64
64
 
65
+ ## Viewing attributes
66
+
67
+ The run details panel in the observability UI shows the run's current attributes as key-value rows. Reserved `$`-prefixed keys are marked with a badge and sorted after user keys:
68
+
69
+ ![Run details panel showing the Attributes card with reserved keys badged](/screenshots/attributes/run-details-attributes.png)
70
+
71
+ Each `experimental_setAttributes` call appears on the trace timeline as a diamond marker at the moment the attributes were written:
72
+
73
+ ![Trace timeline with attr_set diamond markers on the run row](/screenshots/attributes/trace-timeline.png)
74
+
75
+ Expanding an `attr_set` event — in the run sidebar or the Events tab — shows the changed keys, removed keys, and whether the write came from the workflow body or a step (with the attempt number):
76
+
77
+ ![Expanded attr_set events showing changes and the writer](/screenshots/attributes/run-details-attr-set-events.png)
78
+
65
79
  ## Experimental Behavior
66
80
 
67
81
  While attributes are experimental:
@@ -109,7 +109,7 @@ That's it. The plugin automatically:
109
109
 
110
110
  1. Transforms `"use workflow"` and `"use step"` directives via SWC
111
111
  2. Builds workflow and step bundles before tests run
112
- 3. Sets up an in-process workflow runtime using a fresh [Local World](/docs/worlds/local) instance in each test worker — all workflow data is cleared automatically between test files for full isolation
112
+ 3. Sets up an in-process workflow runtime using a fresh [Local World](/worlds/local) instance in each test worker — all workflow data is cleared automatically between test files for full isolation
113
113
 
114
114
  <Callout type="info">
115
115
  Use a separate Vitest configuration and a distinct file naming convention (e.g. `*.integration.test.ts`) to keep unit tests and integration tests separate. Unit tests run with a standard Vitest config without the workflow plugin, while integration tests use the config above.
@@ -117,7 +117,7 @@ Use a separate Vitest configuration and a distinct file naming convention (e.g.
117
117
 
118
118
  ### Writing Integration Tests
119
119
 
120
- Use [`start()`](/docs/api-reference/workflow-api/start) to trigger a workflow and [`run.returnValue`](/docs/api-reference/workflow-api/start#returnvalue) to get the result. `returnValue` is a promise that blocks until the workflow completes (or throws if it fails):
120
+ Use [`start()`](/docs/api-reference/workflow-api/start) to trigger a workflow and [`run.returnValue`](/docs/api-reference/workflow-api/start#returns) to get the result. `returnValue` is a promise that blocks until the workflow completes (or throws if it fails):
121
121
 
122
122
  ```typescript title="workflows/calculate.integration.test.ts" lineNumbers
123
123
  import { describe, it, expect } from "vitest";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "5.0.0-beta.15",
3
+ "version": "5.0.0-beta.16",
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.15",
61
- "@workflow/cli": "5.0.0-beta.15",
62
- "@workflow/core": "5.0.0-beta.15",
60
+ "@workflow/astro": "5.0.0-beta.16",
61
+ "@workflow/cli": "5.0.0-beta.16",
62
+ "@workflow/core": "5.0.0-beta.16",
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.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"
66
+ "@workflow/next": "5.0.0-beta.16",
67
+ "@workflow/nest": "5.0.0-beta.16",
68
+ "@workflow/nitro": "5.0.0-beta.16",
69
+ "@workflow/nuxt": "5.0.0-beta.16",
70
+ "@workflow/sveltekit": "5.0.0-beta.16",
71
+ "@workflow/rollup": "5.0.0-beta.16"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/ms": "2.1.0",