workflow 5.0.0-beta.3 → 5.0.0-beta.30

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 (176) hide show
  1. package/dist/api-workflow.d.ts +1 -1
  2. package/dist/api-workflow.d.ts.map +1 -1
  3. package/dist/api-workflow.js +1 -1
  4. package/dist/api.d.ts +5 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +12 -2
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -1
  10. package/dist/internal/builtins.d.ts +17 -0
  11. package/dist/internal/builtins.d.ts.map +1 -1
  12. package/dist/internal/builtins.js +65 -1
  13. package/dist/observability.d.ts +1 -1
  14. package/dist/observability.js +2 -2
  15. package/dist/runtime.d.ts +2 -1
  16. package/dist/runtime.d.ts.map +1 -1
  17. package/dist/runtime.js +5 -2
  18. package/docs/ai/chat-session-modeling.mdx +7 -3
  19. package/docs/ai/defining-tools.mdx +2 -2
  20. package/docs/ai/index.mdx +28 -23
  21. package/docs/ai/message-queueing.mdx +10 -10
  22. package/docs/ai/resumable-streams.mdx +9 -1
  23. package/docs/api-reference/index.mdx +24 -0
  24. package/docs/api-reference/meta.json +8 -0
  25. package/docs/api-reference/vitest/index.mdx +28 -7
  26. package/docs/api-reference/workflow/create-hook.mdx +38 -0
  27. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  28. package/docs/api-reference/workflow/experimental-set-attributes.mdx +65 -0
  29. package/docs/api-reference/workflow/fetch.mdx +5 -0
  30. package/docs/api-reference/workflow/index.mdx +3 -0
  31. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  32. package/docs/api-reference/workflow-ai/index.mdx +2 -2
  33. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
  34. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
  35. package/docs/api-reference/workflow-api/get-run.mdx +25 -0
  36. package/docs/api-reference/workflow-api/index.mdx +6 -8
  37. package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
  38. package/docs/api-reference/workflow-api/start.mdx +13 -5
  39. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  40. package/docs/api-reference/workflow-astro/meta.json +4 -0
  41. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  42. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  43. package/docs/api-reference/workflow-errors/index.mdx +85 -0
  44. package/docs/api-reference/workflow-errors/meta.json +5 -0
  45. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  46. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  47. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  48. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  49. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  50. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  51. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  52. package/docs/api-reference/workflow-nest/meta.json +9 -0
  53. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  54. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  55. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  56. package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
  57. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  58. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  59. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  60. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  61. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  62. package/docs/api-reference/workflow-observability/meta.json +11 -0
  63. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  64. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  65. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  66. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  67. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  68. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  69. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  70. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  71. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  72. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  73. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  74. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  75. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
  76. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  77. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +4 -2
  78. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
  79. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  80. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  81. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  82. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
  83. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  84. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  85. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  86. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  87. package/docs/api-reference/workflow-vite/meta.json +4 -0
  88. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  89. package/docs/changelog/attributes-mvp.mdx +380 -0
  90. package/docs/changelog/eager-processing.mdx +269 -0
  91. package/docs/changelog/index.mdx +2 -1
  92. package/docs/changelog/lazy-event-creation.md +127 -0
  93. package/docs/changelog/meta.json +8 -1
  94. package/docs/changelog/resilient-start.mdx +31 -283
  95. package/docs/changelog/step-message-ownership.mdx +360 -0
  96. package/docs/changelog/turbo-mode.md +87 -0
  97. package/docs/configuration/build-and-diagnostics.mdx +51 -0
  98. package/docs/configuration/cli-and-web-ui.mdx +154 -0
  99. package/docs/configuration/framework-options.mdx +165 -0
  100. package/docs/configuration/index.mdx +32 -0
  101. package/docs/configuration/meta.json +12 -0
  102. package/docs/configuration/runtime-tuning.mdx +156 -0
  103. package/docs/configuration/worlds.mdx +228 -0
  104. package/docs/cookbook/advanced/child-workflows.mdx +199 -256
  105. package/docs/cookbook/advanced/meta.json +1 -1
  106. package/docs/cookbook/advanced/publishing-libraries.mdx +13 -12
  107. package/docs/cookbook/advanced/serializable-steps.mdx +8 -4
  108. package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
  109. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +31 -76
  110. package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -143
  111. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +7 -3
  112. package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
  113. package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
  114. package/docs/cookbook/common-patterns/saga.mdx +2 -2
  115. package/docs/cookbook/common-patterns/scheduling.mdx +8 -0
  116. package/docs/cookbook/common-patterns/timeouts.mdx +2 -1
  117. package/docs/cookbook/common-patterns/workflow-composition.mdx +15 -16
  118. package/docs/cookbook/index.mdx +3 -3
  119. package/docs/cookbook/integrations/ai-sdk.mdx +60 -28
  120. package/docs/cookbook/integrations/chat-sdk.mdx +13 -0
  121. package/docs/cookbook/integrations/sandbox.mdx +13 -0
  122. package/docs/deploying/building-a-world.mdx +1 -1
  123. package/docs/deploying/index.mdx +1 -0
  124. package/docs/deploying/world/local-world.mdx +23 -6
  125. package/docs/deploying/world/postgres-world.mdx +79 -13
  126. package/docs/deploying/world/vercel-world.mdx +47 -12
  127. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  128. package/docs/errors/corrupted-event-log.mdx +5 -5
  129. package/docs/errors/hook-conflict.mdx +56 -4
  130. package/docs/errors/index.mdx +1 -35
  131. package/docs/errors/replay-divergence.mdx +27 -0
  132. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  133. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  134. package/docs/errors/step-not-registered.mdx +1 -1
  135. package/docs/foundations/cancellation.mdx +459 -0
  136. package/docs/foundations/errors-and-retries.mdx +7 -3
  137. package/docs/foundations/hooks.mdx +29 -0
  138. package/docs/foundations/idempotency.mdx +236 -11
  139. package/docs/foundations/index.mdx +1 -23
  140. package/docs/foundations/meta.json +3 -1
  141. package/docs/foundations/serialization.mdx +77 -41
  142. package/docs/foundations/starting-workflows.mdx +5 -1
  143. package/docs/foundations/streaming.mdx +14 -23
  144. package/docs/foundations/versioning.mdx +263 -0
  145. package/docs/getting-started/astro.mdx +6 -0
  146. package/docs/getting-started/index.mdx +6 -7
  147. package/docs/getting-started/meta.json +1 -0
  148. package/docs/getting-started/nestjs.mdx +9 -0
  149. package/docs/getting-started/next.mdx +5 -3
  150. package/docs/getting-started/nitro.mdx +22 -0
  151. package/docs/getting-started/sveltekit.mdx +6 -0
  152. package/docs/getting-started/tanstack-start.mdx +241 -0
  153. package/docs/how-it-works/cancellation.mdx +287 -0
  154. package/docs/how-it-works/code-transform.mdx +2 -2
  155. package/docs/how-it-works/encryption.mdx +2 -2
  156. package/docs/how-it-works/event-sourcing.mdx +2 -2
  157. package/docs/how-it-works/meta.json +2 -1
  158. package/docs/internal/index.mdx +21 -0
  159. package/docs/internal/meta.json +10 -0
  160. package/docs/internal/nitro-native-build.mdx +38 -0
  161. package/docs/internal/nitro-web-ui.mdx +24 -0
  162. package/docs/internal/serializable-abort-controller.mdx +148 -0
  163. package/docs/meta.json +1 -1
  164. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
  165. package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
  166. package/docs/migration-guides/migrating-from-temporal.mdx +6 -11
  167. package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
  168. package/docs/observability/attributes.mdx +87 -0
  169. package/docs/observability/index.mdx +18 -1
  170. package/docs/observability/meta.json +1 -1
  171. package/docs/observability/tracing.mdx +119 -0
  172. package/docs/testing/index.mdx +2 -2
  173. package/package.json +14 -13
  174. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  175. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  176. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
@@ -11,7 +11,6 @@ The `@workflow/vitest` package provides a Vitest plugin and test helpers for run
11
11
 
12
12
  Returns a Vite plugin array that handles SWC transforms, bundle building, and in-process handler registration automatically.
13
13
 
14
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
15
14
 
16
15
  ```typescript
17
16
  import { defineConfig } from "vitest/config";
@@ -22,6 +21,29 @@ export default defineConfig({
22
21
  });
23
22
  ```
24
23
 
24
+ Pass a [`WorkflowTestOptions`](#workflowtestoptions) object when your project uses a non-standard layout — for example, a monorepo where `workflows/` does not live at the Vitest config's directory, or when the default `.workflow-data` / `.workflow-vitest` output locations need to move. The plugin forwards these paths to `buildWorkflowTests()` and `setupWorkflowTests()` through Vitest's per-project provided context, so each Vitest workspace project stays isolated.
25
+
26
+
27
+ ```typescript
28
+ import { defineConfig } from "vitest/config";
29
+ import { workflow } from "@workflow/vitest";
30
+
31
+ export default defineConfig({
32
+ plugins: [
33
+ workflow({
34
+ cwd: "./apps/api",
35
+ rootDir: "./apps/api/test-artifacts",
36
+ }),
37
+ ],
38
+ });
39
+ ```
40
+
41
+ **Parameters:**
42
+
43
+ | Parameter | Type | Description |
44
+ | --- | --- | --- |
45
+ | `options?` | `WorkflowTestOptions` | Optional configuration |
46
+
25
47
  **Returns:** `Plugin[]`
26
48
 
27
49
  ## Setup Functions
@@ -30,7 +52,6 @@ export default defineConfig({
30
52
 
31
53
  Builds workflow and step bundles to disk. Called automatically by the `workflow()` plugin in `globalSetup`. Use directly only for [manual setup](/docs/testing#manual-setup).
32
54
 
33
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
34
55
 
35
56
  ```typescript
36
57
  import { buildWorkflowTests } from "@workflow/vitest";
@@ -48,11 +69,10 @@ export async function setup() {
48
69
 
49
70
  ### `setupWorkflowTests()`
50
71
 
51
- 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.
52
73
 
53
74
  Called automatically by the `workflow()` plugin in `setupFiles`. Use directly only for [manual setup](/docs/testing#manual-setup).
54
75
 
55
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
56
76
 
57
77
  ```typescript
58
78
  import { beforeAll, afterAll } from "vitest";
@@ -83,7 +103,10 @@ Tears down the workflow test world. Clears the global world and closes the Local
83
103
 
84
104
  | Option | Type | Default | Description |
85
105
  | --- | --- | --- | --- |
86
- | `cwd` | `string` | `process.cwd()` | The working directory of the project (where `workflows/` lives) |
106
+ | `cwd` | `string` | `process.cwd()` | The working directory of the project (where `workflows/` lives). Relative paths resolve against `process.cwd()`. |
107
+ | `rootDir` | `string` | same as `cwd` | Root directory used for default test artifacts. When set, `dataDir` and `outDir` default to `<rootDir>/.workflow-data` and `<rootDir>/.workflow-vitest`. Relative paths resolve against `cwd`. |
108
+ | `dataDir` | `string` | `<rootDir>/.workflow-data` | Directory for workflow runtime data written by the test world. Relative paths resolve against `cwd`. |
109
+ | `outDir` | `string` | `<rootDir>/.workflow-vitest` | Directory for generated workflow and step bundles. Relative paths resolve against `cwd`. |
87
110
 
88
111
  ## Test Helpers
89
112
 
@@ -91,7 +114,6 @@ Tears down the workflow test world. Clears the global world and closes the Local
91
114
 
92
115
  Polls the event log until the workflow has a pending `sleep()` call — one with a `wait_created` event but no corresponding `wait_completed` event. Returns the correlation ID of the pending sleep, which can be passed to [`wakeUp()`](/docs/api-reference/workflow-api/get-run) to target a specific sleep.
93
116
 
94
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
95
117
 
96
118
  ```typescript
97
119
  import { waitForSleep } from "@workflow/vitest"; // [!code highlight]
@@ -120,7 +142,6 @@ await getRun(run.runId).wakeUp({ correlationIds: [sleepId] }); // [!code highlig
120
142
 
121
143
  Polls the hook list and event log until a hook matching the optional `token` filter exists that hasn't been received yet. Returns the matching hook object.
122
144
 
123
- {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
124
145
 
125
146
  ```typescript
126
147
  import { waitForHook } from "@workflow/vitest"; // [!code highlight]
@@ -8,6 +8,7 @@ prerequisites:
8
8
  related:
9
9
  - /docs/api-reference/workflow/define-hook
10
10
  - /docs/api-reference/workflow/create-webhook
11
+ - /docs/foundations/idempotency
11
12
  ---
12
13
 
13
14
  Creates a low-level hook primitive that can be used to resume a workflow run with arbitrary payloads.
@@ -65,6 +66,8 @@ export default Hook;`}
65
66
 
66
67
  The returned `Hook` object also implements `AsyncIterable<T>`, which allows you to iterate over incoming payloads using `for await...of` syntax.
67
68
 
69
+ Use `hook.getConflict()` to check whether the hook token is already claimed by another active hook, without waiting for hook payload data. Calling `createHook()` on its own does not register the hook — registration is only committed when the workflow suspends. Awaiting `hook.getConflict()` suspends the workflow to commit the registration, then resolves with `null` once `hook_created` is recorded, or with the conflicting [`Run`](/docs/api-reference/workflow-api/get-run) if another active hook already owns the same token.
70
+
68
71
  ## Examples
69
72
 
70
73
  ### Basic Usage
@@ -112,6 +115,40 @@ export async function slackBotWorkflow(channelId: string) {
112
115
  }
113
116
  ```
114
117
 
118
+ ### Detecting Token Conflicts
119
+
120
+ Use `hook.getConflict()` when the workflow needs to claim a hook token before doing other work, but does not need a payload yet:
121
+
122
+ ```typescript lineNumbers
123
+ import { createHook } from "workflow";
124
+
125
+ declare function chargeOrder(orderId: string): Promise<void>; // @setup
126
+
127
+ async function processOrder(orderId: string) {
128
+ "use workflow";
129
+
130
+ using hook = createHook({ // [!code highlight]
131
+ token: `order:${orderId}` // [!code highlight]
132
+ }); // [!code highlight]
133
+
134
+ const conflict = await hook.getConflict(); // [!code highlight]
135
+ if (conflict) { // [!code highlight]
136
+ // Another active workflow run already owns this token.
137
+ return { dedupedTo: conflict.runId };
138
+ }
139
+
140
+ await chargeOrder(orderId);
141
+ }
142
+ ```
143
+
144
+ Because `createHook()` alone does not suspend the workflow, awaiting `hook.getConflict()` is what actually suspends the run and commits the hook registration. It only waits for registration — to receive payload data from a future `resumeHook()` call, await the hook itself or iterate it with `for await...of`.
145
+
146
+ On a conflict, the resolved value is a `Run` handle for the run that currently owns the token, with durable step-backed accessors. The duplicate run can decide in code how to handle it: return or log `conflict.runId`, inspect `await conflict.status`, wait on `await conflict.returnValue`, or cancel the owner with `await conflict.cancel()` and continue in the current run. See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for these strategies in context.
147
+
148
+ <Callout type="info">
149
+ Custom hook tokens are the recommended way to coordinate active workflow runs. Use a deterministic token from your domain, such as an order ID or conversation ID, create the hook near the beginning of the workflow, and check `await hook.getConflict()` before work that depends on owning the token. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
150
+ </Callout>
151
+
115
152
  ### Waiting for Multiple Payloads
116
153
 
117
154
  You can also wait for multiple payloads by using the `for await...of` syntax.
@@ -195,3 +232,4 @@ This is equivalent to manually calling `dispose()` but ensures the hook is alway
195
232
  - [`defineHook()`](/docs/api-reference/workflow/define-hook) - Type-safe hook helper
196
233
  - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) - Resume a hook with a payload
197
234
  - [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Higher-level HTTP webhook abstraction
235
+ - [Idempotency](/docs/foundations/idempotency) - Deduplicate step side effects and workflow starts
@@ -55,6 +55,7 @@ The returned `Webhook` object has:
55
55
 
56
56
  - `url`: The HTTP endpoint URL that external systems can call
57
57
  - `token`: The unique token identifying this webhook
58
+ - `getConflict()`: A promise that resolves with the conflicting run if another active hook already owns this token, or `null` once the webhook endpoint has been registered
58
59
  - Implements `AsyncIterable<T>` for handling multiple requests, where `T` is `Request` (default) or `RequestWithResponse` (manual mode)
59
60
 
60
61
  When using `createWebhook({ respondWith: 'manual' })`, the resolved request type is `RequestWithResponse`, which extends the standard `Request` interface with a `respondWith(response: Response): Promise<void>` method for sending custom responses back to the caller.
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: experimental_setAttributes
3
+ description: Attach string metadata to workflow run for observability.
4
+ type: reference
5
+ summary: Use experimental_setAttributes inside a workflow or step function to set run attributes.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ related:
9
+ - /docs/observability/attributes
10
+ - /docs/api-reference/workflow/fatal-error
11
+ ---
12
+
13
+ Attaches string metadata to the current workflow run.
14
+
15
+ <Callout>
16
+ This API is experimental and may change before the stable attributes API is released.
17
+ </Callout>
18
+
19
+ ```typescript lineNumbers
20
+ import { experimental_setAttributes } from "workflow"
21
+
22
+ export async function orderWorkflow(orderId: string) {
23
+ "use workflow"
24
+
25
+ await experimental_setAttributes({
26
+ phase: "received",
27
+ orderId,
28
+ })
29
+ }
30
+ ```
31
+
32
+ ## API Signature
33
+
34
+ ### Parameters
35
+
36
+ <TSDoc
37
+ definition={`
38
+ import { experimental_setAttributes } from "workflow";
39
+ export default experimental_setAttributes;`}
40
+ showSections={['parameters']}
41
+ />
42
+
43
+ ## Usage
44
+
45
+ Call `experimental_setAttributes` from a `"use workflow"` function or a `"use step"` function. Calling it from plain application code is not supported because there is no active workflow run.
46
+
47
+ Attribute values must be strings. Pass `undefined` to remove an attribute:
48
+
49
+ ```typescript lineNumbers
50
+ import { experimental_setAttributes } from "workflow"
51
+
52
+ export async function cleanupAttributes() {
53
+ "use workflow"
54
+
55
+ await experimental_setAttributes({ staleKey: undefined })
56
+ }
57
+ ```
58
+
59
+ Attribute keys must be 1-256 characters, values must be strings up to 256 bytes, and each run can have up to 64 attributes. Keys that start with `$` are reserved for framework and library code.
60
+
61
+ Validation errors throw [`FatalError`](/docs/api-reference/workflow/fatal-error) and fail the run before an attribute write is attempted.
62
+
63
+ Calls from both workflow and step bodies append a native `attr_set` event, which the World materializes onto `run.attributes`. Workflow-originated events record a workflow writer; step-originated events record the originating step ID and attempt.
64
+
65
+ Native attributes require spec version 4 or later. Step-body storage errors throw from `experimental_setAttributes`; catch them inside the step if the write should be best-effort. Workflow-body writes are committed when the workflow suspends: transient storage errors are retried with the suspension, while a write the World rejects as invalid — such as exceeding the per-run attribute cap across multiple calls — fails the run with the validation error.
@@ -7,12 +7,17 @@ prerequisites:
7
7
  - /docs/foundations/workflows-and-steps
8
8
  related:
9
9
  - /docs/errors/fetch-in-workflow
10
+ - /docs/foundations/idempotency
10
11
  ---
11
12
 
12
13
  Makes HTTP requests from within a workflow. This is a special step function that wraps the standard `fetch` API, automatically handling serialization and providing retry semantics.
13
14
 
14
15
  This is useful when you need to call external APIs or services from within your workflow.
15
16
 
17
+ <Callout type="warn">
18
+ Because workflow `fetch()` has retry semantics, use idempotency keys when the request mutates an external system, such as creating a charge, sending an email, or enqueueing work. See [Idempotency](/docs/foundations/idempotency).
19
+ </Callout>
20
+
16
21
  <Callout>
17
22
  `fetch` is a *special* type of step function provided and should be called directly inside workflow functions.
18
23
  </Callout>
@@ -47,6 +47,9 @@ Workflow SDK contains the following functions you can use inside your workflow f
47
47
  <Card href="/docs/api-reference/workflow/get-writable" title="getWritable()">
48
48
  Access the current workflow run's default stream.
49
49
  </Card>
50
+ <Card href="/docs/api-reference/workflow/experimental-set-attributes" title="experimental_setAttributes()">
51
+ Attach experimental string metadata to the current workflow run.
52
+ </Card>
50
53
  </Cards>
51
54
 
52
55
  ## Error Classes
@@ -1,59 +1,21 @@
1
1
  ---
2
2
  title: DurableAgent
3
- description: Create AI agents that maintain state, call tools, and handle interruptions gracefully.
3
+ description: Deprecated DurableAgent API reference; use WorkflowAgent for new durable agents.
4
4
  type: reference
5
- summary: Use DurableAgent to build AI agents that maintain state across steps and survive interruptions.
5
+ summary: "Deprecated: use AI SDK's WorkflowAgent instead of DurableAgent."
6
6
  prerequisites:
7
7
  - /docs/ai
8
8
  related:
9
9
  - /docs/ai/defining-tools
10
10
  ---
11
11
 
12
- The `DurableAgent` class enables you to create AI-powered agents that can maintain state across workflow steps, call tools, and gracefully handle interruptions and resumptions.
13
-
14
- Tool calls can be implemented as workflow steps for automatic retries, or as regular workflow-level logic utilizing core library features such as [`sleep()`](/docs/api-reference/workflow/sleep) and [Hooks](/docs/foundations/hooks).
15
-
16
- ```typescript lineNumbers
17
- import { DurableAgent } from "@workflow/ai/agent";
18
- import { getWritable } from "workflow";
19
- import { z } from "zod";
20
- import type { UIMessageChunk } from "ai";
21
-
22
- async function getWeather({ city }: { city: string }) {
23
- "use step";
24
-
25
- return `Weather in ${city} is sunny`;
26
- }
27
-
28
- async function myAgent() {
29
- "use workflow";
30
-
31
- const agent = new DurableAgent({
32
- model: "anthropic/claude-haiku-4.5",
33
- instructions: "You are a helpful weather assistant.",
34
- temperature: 0.7,
35
- tools: {
36
- getWeather: {
37
- description: "Get weather for a city",
38
- inputSchema: z.object({ city: z.string() }),
39
- execute: getWeather,
40
- },
41
- },
42
- });
43
-
44
- // The agent will stream its output to the workflow
45
- // run's default output stream
46
- const writable = getWritable<UIMessageChunk>();
12
+ <Callout type="warn">
13
+ `DurableAgent` is deprecated. Use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for new durable agents — see the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent).
14
+ </Callout>
47
15
 
48
- const result = await agent.stream({
49
- messages: [{ role: "user", content: "How is the weather in San Francisco?" }],
50
- writable,
51
- });
16
+ This reference is kept for existing applications that still import `DurableAgent` from `@workflow/ai/agent`. Do not use `DurableAgent` for new code.
52
17
 
53
- // result contains messages, steps, and optional structured output
54
- console.log(result.messages);
55
- }
56
- ```
18
+ For current examples and implementation guidance, see AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) docs. For legacy code, the API surface below documents the existing `DurableAgent` exports.
57
19
 
58
20
  ## API Signature
59
21
 
@@ -13,9 +13,9 @@ Helpers for integrating AI SDK for building AI-powered workflows.
13
13
 
14
14
  <Cards>
15
15
  <Card title="DurableAgent" href="/docs/api-reference/workflow-ai/durable-agent">
16
- A class for building durable AI agents that maintain state across workflow steps and handle tool execution with automatic retries.
16
+ Deprecated use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent). Reference kept for existing `@workflow/ai/agent` imports.
17
17
  </Card>
18
18
  <Card title="WorkflowChatTransport" href="/docs/api-reference/workflow-ai/workflow-chat-transport">
19
- A drop-in transport for the AI SDK for automatic reconnection in interrupted streams.
19
+ Deprecated use AI SDK's [`WorkflowChatTransport`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport) from `@ai-sdk/workflow`. Reference kept for existing `@workflow/ai` imports.
20
20
  </Card>
21
21
  </Cards>
@@ -9,6 +9,10 @@ related:
9
9
  - /docs/ai/resumable-streams
10
10
  ---
11
11
 
12
+ <Callout type="warn">
13
+ `WorkflowChatTransport` from `@workflow/ai` is deprecated. AI SDK ships a 1:1 port — use [`WorkflowChatTransport` from `@ai-sdk/workflow`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport) instead. This reference is kept for existing applications that still import it from `@workflow/ai`.
14
+ </Callout>
15
+
12
16
  A chat transport implementation for the AI SDK that provides reliable message streaming with automatic reconnection to interrupted streams. This transport is a drop-in replacement for the default AI SDK transport, enabling seamless recovery from network issues, page refreshes, or Vercel Function timeouts.
13
17
 
14
18
  <Callout>
@@ -250,9 +254,48 @@ export default function ChatWithCustomConfig() {
250
254
  }
251
255
  ```
252
256
 
257
+ ## Mid-part resumes
258
+
259
+ A workflow stream is a flat sequence of chunks, but the AI SDK's UI protocol groups chunks into logical parts: a `text-start` opens a text part that subsequent `text-delta`s extend and a `text-end` closes, and the same shape applies to `reasoning-*` and `tool-input-*`. The AI SDK client enforces that grammar — a `reasoning-delta` whose `reasoning-start` was never seen throws and breaks the chat.
260
+
261
+ A non-zero `startIndex` (in particular a negative `initialStartIndex`) resolves to a chunk offset with no awareness of those part boundaries, so it can land in the middle of an open part. When that happens, `WorkflowChatTransport` will **drop chunks that reference a part it didn't see a start for** and log a one-time warning. The chat keeps working, but any partial part overlapping the resume cursor is discarded. Tool calls are an exception: `tool-input-available` / `tool-input-error` chunks are self-contained (they carry the full input), so a tool call is recovered as soon as one of those chunks appears in the resumed window — only its streamed input deltas are lost.
262
+
263
+ To preserve those partial parts, rewind to a step boundary on the server before returning the readable. `start-step` / `finish-step` chunks are the natural seams — no UI part is ever open across them. Sketch:
264
+
265
+ {/*@skip-typecheck: incomplete code sample*/}
266
+
267
+ ```typescript title="app/api/chat/[id]/stream/route.ts"
268
+ const run = getRun(id);
269
+ const tailIndex = await run.getReadable().getTailIndex();
270
+
271
+ let resolved = startIndex < 0
272
+ ? Math.max(0, tailIndex + 1 + startIndex)
273
+ : startIndex;
274
+
275
+ if (startIndex !== 0) {
276
+ // Walk back from `resolved` to the most recent start-step (or chunk 0),
277
+ // capping the lookback so a single huge step can't trigger an unbounded scan.
278
+ const LOOKBACK = 200;
279
+ const probe = run.getReadable({ startIndex: Math.max(0, resolved - LOOKBACK) });
280
+ let i = Math.max(0, resolved - LOOKBACK);
281
+ let lastBoundary = i;
282
+ for await (const chunk of probe as unknown as AsyncIterable<{ type: string }>) {
283
+ if (i >= resolved) break;
284
+ if (chunk.type === "start-step") lastBoundary = i;
285
+ i++;
286
+ }
287
+ resolved = lastBoundary;
288
+ }
289
+
290
+ return createUIMessageStreamResponse({
291
+ stream: run.getReadable({ startIndex: resolved }),
292
+ headers: { "x-workflow-stream-tail-index": String(tailIndex) },
293
+ });
294
+ ```
295
+
253
296
  ## See Also
254
297
 
255
- - [DurableAgent](/docs/api-reference/workflow-ai/durable-agent) - Building durable AI agents within workflows
298
+ - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - Building durable, resumable AI agents (replaces `DurableAgent`)
256
299
  - [AI SDK `useChat` Documentation](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) - Using `useChat` with custom transports
257
300
  - [Workflows and Steps](/docs/foundations/workflows-and-steps) - Understanding workflow fundamentals
258
301
  - ["flight-booking-app" Example](https://github.com/vercel/workflow-examples/tree/main/flight-booking-app) - An example application which uses `WorkflowChatTransport`
@@ -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
 
@@ -164,6 +170,25 @@ const { stoppedCount } = await run.wakeUp({
164
170
  });
165
171
  ```
166
172
 
173
+ ### Cancel a Run
174
+
175
+ Cancel a workflow run. You can pass an optional free-text `cancelReason` (up to 512 characters) that is recorded on the run's cancellation event and shown in the run detail view:
176
+
177
+ ```typescript lineNumbers
178
+ import { getRun } from "workflow/api";
179
+
180
+ export async function POST(req: Request) {
181
+ const { runId } = await req.json();
182
+ const run = getRun(runId);
183
+
184
+ await run.cancel({ cancelReason: "Superseded by a newer submission" }); // [!code highlight]
185
+
186
+ return Response.json({ cancelled: true });
187
+ }
188
+ ```
189
+
190
+ The options object is optional — `await run.cancel()` cancels the run without recording a reason.
191
+
167
192
  ## Related Functions
168
193
 
169
194
  - [`start()`](/docs/api-reference/workflow-api/start) - Start a new workflow and get its run ID.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "workflow/api"
3
- description: Runtime functions to inspect runs, start workflows, and access world data.
3
+ description: Runtime functions to inspect runs, start workflows, and manage hooks.
4
4
  type: overview
5
5
  summary: Explore runtime functions for starting workflows, inspecting runs, and managing hooks.
6
6
  ---
@@ -9,7 +9,7 @@ API reference for runtime functions from the `workflow/api` package.
9
9
 
10
10
  ## Functions
11
11
 
12
- The API package is for access and introspection of workflow data to inspect runs, start new runs, or access anything else directly accessible by the world.
12
+ The API package is for access and introspection of workflow data to inspect runs, start new runs, and manage hooks.
13
13
 
14
14
  <Cards>
15
15
  <Card href="/docs/api-reference/workflow-api/start" title="start()">
@@ -27,10 +27,8 @@ The API package is for access and introspection of workflow data to inspect runs
27
27
  <Card href="/docs/api-reference/workflow-api/get-run" title="getRun()">
28
28
  Get workflow run status and metadata without waiting for completion.
29
29
  </Card>
30
- <Card href="/docs/api-reference/workflow-api/get-world" title="getWorld()">
31
- Async: resolve the World instance for storage, queuing, and streaming backends.
32
- </Card>
33
- <Card href="/docs/api-reference/workflow-api/world" title="World SDK">
34
- Low-level API for inspecting runs, steps, events, hooks, streams, and queues.
35
- </Card>
36
30
  </Cards>
31
+
32
+ <Callout type="info">
33
+ Looking for `getWorld()` and the World SDK? They are exported from `workflow/runtime` — see the [`workflow/runtime` reference](/docs/api-reference/workflow-runtime).
34
+ </Callout>
@@ -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.
@@ -2,9 +2,11 @@
2
2
  title: start
3
3
  description: Start and enqueue a new workflow run.
4
4
  type: reference
5
- summary: Use start to programmatically enqueue a new workflow run from outside a workflow function.
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.
@@ -50,11 +52,14 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-
50
52
 
51
53
  ## Good to Know
52
54
 
53
- * The `start()` function is used in runtime/non-workflow contexts to programmatically trigger workflow executions.
55
+ * The `start()` function is used in runtime contexts to programmatically trigger workflow executions.
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).
54
57
  * This is different from calling workflow functions directly, which is the typical pattern in Next.js applications.
55
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).
56
60
  * All arguments must be [serializable](/docs/foundations/serialization).
57
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.
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.
58
63
 
59
64
  <Callout type="info">
60
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).
@@ -78,13 +83,14 @@ import { start } from "workflow/api";
78
83
  import { myWorkflow } from "./workflows/my-workflow";
79
84
 
80
85
  const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
81
- deploymentId: "custom-deployment-id" // [!code highlight]
86
+ deploymentId: "custom-deployment-id", // [!code highlight]
87
+ attributes: { source: "checkout" } // [!code highlight]
82
88
  }); // [!code highlight]
83
89
  ```
84
90
 
85
91
  ### Using `deploymentId: "latest"`
86
92
 
87
- Set `deploymentId` to `"latest"` to automatically resolve the most recent deployment for the current environment. This is useful when you want to ensure a workflow run targets the latest deployed version of your application rather than the deployment that initiated the call.
93
+ Set `deploymentId` to `"latest"` to automatically resolve the most recent deployment for the current environment. This is useful when you want to ensure a workflow run targets the latest deployed version of your application rather than the deployment that initiated the call. For when to use this and how it fits with default run pinning, see [Versioning](/docs/foundations/versioning).
88
94
 
89
95
  ```typescript
90
96
  import { start } from "workflow/api";
@@ -96,7 +102,9 @@ const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
96
102
  ```
97
103
 
98
104
  <Callout type="info">
99
- The `deploymentId` option is currently a Vercel-specific feature. The `"latest"` value resolves to the most recent deployment matching your current environment — the same production target for production deployments, or the same git branch for preview deployments.
105
+ The `deploymentId` option is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment — the same production target for production deployments, or the same git branch for preview deployments.
106
+
107
+ In Worlds without atomic, immutable deployments (such as local development or self-hosted Postgres), there is no notion of multiple deployments to resolve between, so `deploymentId: "latest"` has no effect: the SDK logs a warning and the run targets the current deployment. This means a workflow that opts into `"latest"` on Vercel still runs unchanged in local development.
100
108
  </Callout>
101
109
 
102
110
  <Callout type="warn">