workflow 4.2.4 → 4.2.5

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.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * import { getWorld } from 'workflow/api';
9
+ * import { getWorld } from 'workflow/runtime';
10
10
  * import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
11
11
  *
12
12
  * const world = getWorld();
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * import { getWorld } from 'workflow/api';
9
+ * import { getWorld } from 'workflow/runtime';
10
10
  * import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
11
11
  *
12
12
  * const world = getWorld();
@@ -17,4 +17,4 @@
17
17
  */
18
18
  export { hydrateData, hydrateResourceIO, observabilityRevivers, } from '@workflow/core/serialization-format';
19
19
  export { parseClassName, parseStepName, parseWorkflowName, } from '@workflow/utils';
20
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2YWJpbGl0eS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vYnNlcnZhYmlsaXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsT0FBTyxFQUNMLFdBQVcsRUFDWCxpQkFBaUIsRUFDakIscUJBQXFCLEdBRXRCLE1BQU0scUNBQXFDLENBQUM7QUFFN0MsT0FBTyxFQUNMLGNBQWMsRUFDZCxhQUFhLEVBQ2IsaUJBQWlCLEdBQ2xCLE1BQU0saUJBQWlCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIE9ic2VydmFiaWxpdHkgdXRpbGl0aWVzIGZvciBoeWRyYXRpbmcgc2VyaWFsaXplZCB3b3JrZmxvdyBkYXRhLlxuICpcbiAqIFVzZSB0aGVzZSB3aGVuIGluc3BlY3Rpbmcgd29ya2Zsb3cgc3RlcCBJL08sIHJ1biBpbnB1dHMvb3V0cHV0cyxcbiAqIG9yIGV2ZW50IGRhdGEgZnJvbSB0aGUgV29ya2Zsb3cgU0RLJ3Mgd29ybGQgQVBJcy5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHNcbiAqIGltcG9ydCB7IGdldFdvcmxkIH0gZnJvbSAnd29ya2Zsb3cvYXBpJztcbiAqIGltcG9ydCB7IGh5ZHJhdGVSZXNvdXJjZUlPLCBvYnNlcnZhYmlsaXR5UmV2aXZlcnMgfSBmcm9tICd3b3JrZmxvdy9vYnNlcnZhYmlsaXR5JztcbiAqXG4gKiBjb25zdCB3b3JsZCA9IGdldFdvcmxkKCk7XG4gKiBjb25zdCBzdGVwID0gYXdhaXQgd29ybGQuc3RlcHMuZ2V0KHJ1bklkLCBzdGVwSWQsIHsgcmVzb2x2ZURhdGE6ICdhbGwnIH0pO1xuICogY29uc3QgaHlkcmF0ZWQgPSBoeWRyYXRlUmVzb3VyY2VJTyhzdGVwLCBvYnNlcnZhYmlsaXR5UmV2aXZlcnMpO1xuICogLy8gaHlkcmF0ZWQuaW5wdXQgYW5kIGh5ZHJhdGVkLm91dHB1dCBhcmUgbm93IHBsYWluIEpTIG9iamVjdHNcbiAqIGBgYFxuICovXG5leHBvcnQge1xuICBoeWRyYXRlRGF0YSxcbiAgaHlkcmF0ZVJlc291cmNlSU8sXG4gIG9ic2VydmFiaWxpdHlSZXZpdmVycyxcbiAgdHlwZSBSZXZpdmVycyxcbn0gZnJvbSAnQHdvcmtmbG93L2NvcmUvc2VyaWFsaXphdGlvbi1mb3JtYXQnO1xuXG5leHBvcnQge1xuICBwYXJzZUNsYXNzTmFtZSxcbiAgcGFyc2VTdGVwTmFtZSxcbiAgcGFyc2VXb3JrZmxvd05hbWUsXG59IGZyb20gJ0B3b3JrZmxvdy91dGlscyc7XG4iXX0=
20
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2YWJpbGl0eS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vYnNlcnZhYmlsaXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsT0FBTyxFQUNMLFdBQVcsRUFDWCxpQkFBaUIsRUFDakIscUJBQXFCLEdBRXRCLE1BQU0scUNBQXFDLENBQUM7QUFFN0MsT0FBTyxFQUNMLGNBQWMsRUFDZCxhQUFhLEVBQ2IsaUJBQWlCLEdBQ2xCLE1BQU0saUJBQWlCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIE9ic2VydmFiaWxpdHkgdXRpbGl0aWVzIGZvciBoeWRyYXRpbmcgc2VyaWFsaXplZCB3b3JrZmxvdyBkYXRhLlxuICpcbiAqIFVzZSB0aGVzZSB3aGVuIGluc3BlY3Rpbmcgd29ya2Zsb3cgc3RlcCBJL08sIHJ1biBpbnB1dHMvb3V0cHV0cyxcbiAqIG9yIGV2ZW50IGRhdGEgZnJvbSB0aGUgV29ya2Zsb3cgU0RLJ3Mgd29ybGQgQVBJcy5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHNcbiAqIGltcG9ydCB7IGdldFdvcmxkIH0gZnJvbSAnd29ya2Zsb3cvcnVudGltZSc7XG4gKiBpbXBvcnQgeyBoeWRyYXRlUmVzb3VyY2VJTywgb2JzZXJ2YWJpbGl0eVJldml2ZXJzIH0gZnJvbSAnd29ya2Zsb3cvb2JzZXJ2YWJpbGl0eSc7XG4gKlxuICogY29uc3Qgd29ybGQgPSBnZXRXb3JsZCgpO1xuICogY29uc3Qgc3RlcCA9IGF3YWl0IHdvcmxkLnN0ZXBzLmdldChydW5JZCwgc3RlcElkLCB7IHJlc29sdmVEYXRhOiAnYWxsJyB9KTtcbiAqIGNvbnN0IGh5ZHJhdGVkID0gaHlkcmF0ZVJlc291cmNlSU8oc3RlcCwgb2JzZXJ2YWJpbGl0eVJldml2ZXJzKTtcbiAqIC8vIGh5ZHJhdGVkLmlucHV0IGFuZCBoeWRyYXRlZC5vdXRwdXQgYXJlIG5vdyBwbGFpbiBKUyBvYmplY3RzXG4gKiBgYGBcbiAqL1xuZXhwb3J0IHtcbiAgaHlkcmF0ZURhdGEsXG4gIGh5ZHJhdGVSZXNvdXJjZUlPLFxuICBvYnNlcnZhYmlsaXR5UmV2aXZlcnMsXG4gIHR5cGUgUmV2aXZlcnMsXG59IGZyb20gJ0B3b3JrZmxvdy9jb3JlL3NlcmlhbGl6YXRpb24tZm9ybWF0JztcblxuZXhwb3J0IHtcbiAgcGFyc2VDbGFzc05hbWUsXG4gIHBhcnNlU3RlcE5hbWUsXG4gIHBhcnNlV29ya2Zsb3dOYW1lLFxufSBmcm9tICdAd29ya2Zsb3cvdXRpbHMnO1xuIl19
package/docs/ai/index.mdx CHANGED
@@ -120,16 +120,17 @@ The core code that makes all of this happen is quite simple. Here's a breakdown
120
120
 
121
121
  <Tab value="API Route">
122
122
 
123
- Our API route makes a simple call to [AI SDK's `Agent` class](https://ai-sdk.dev/docs/agents/overview), which is a simple wrapper around [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.
123
+ Our API route makes a simple call to [AI SDK's `ToolLoopAgent` class](https://ai-sdk.dev/docs/agents/overview), which encapsulates the LLM call, tool execution loop, and stopping conditions on top of [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.
124
124
 
125
125
  ```typescript title="app/api/chat/route.ts" lineNumbers
126
- import { Experimental_Agent as Agent } from "ai";
127
- import type { LanguageModel } from "ai";
126
+ import { ToolLoopAgent } from "ai";
127
+ import type { UIMessage } from "ai";
128
+ import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
128
129
 
129
130
  export async function POST(req: Request) {
130
131
  const { messages }: { messages: UIMessage[] } = await req.json();
131
- const agent = new Agent({ // [!code highlight]
132
- model: gateway("bedrock/claude-4-5-haiku-20251001-v1"),
132
+ const agent = new ToolLoopAgent({ // [!code highlight]
133
+ model: "bedrock/claude-4-5-haiku-20251001-v1",
133
134
  instructions: FLIGHT_ASSISTANT_PROMPT,
134
135
  tools: flightBookingTools,
135
136
  });
@@ -22,6 +22,30 @@ export default defineConfig({
22
22
  });
23
23
  ```
24
24
 
25
+ 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.
26
+
27
+ {/* @skip-typecheck - @workflow/vitest not available in docs-typecheck */}
28
+
29
+ ```typescript
30
+ import { defineConfig } from "vitest/config";
31
+ import { workflow } from "@workflow/vitest";
32
+
33
+ export default defineConfig({
34
+ plugins: [
35
+ workflow({
36
+ cwd: "./apps/api",
37
+ rootDir: "./apps/api/test-artifacts",
38
+ }),
39
+ ],
40
+ });
41
+ ```
42
+
43
+ **Parameters:**
44
+
45
+ | Parameter | Type | Description |
46
+ | --- | --- | --- |
47
+ | `options?` | `WorkflowTestOptions` | Optional configuration |
48
+
25
49
  **Returns:** `Plugin[]`
26
50
 
27
51
  ## Setup Functions
@@ -83,7 +107,10 @@ Tears down the workflow test world. Clears the global world and closes the Local
83
107
 
84
108
  | Option | Type | Default | Description |
85
109
  | --- | --- | --- | --- |
86
- | `cwd` | `string` | `process.cwd()` | The working directory of the project (where `workflows/` lives) |
110
+ | `cwd` | `string` | `process.cwd()` | The working directory of the project (where `workflows/` lives). Relative paths resolve against `process.cwd()`. |
111
+ | `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`. |
112
+ | `dataDir` | `string` | `<rootDir>/.workflow-data` | Directory for workflow runtime data written by the test world. Relative paths resolve against `cwd`. |
113
+ | `outDir` | `string` | `<rootDir>/.workflow-vitest` | Directory for generated workflow and step bundles. Relative paths resolve against `cwd`. |
87
114
 
88
115
  ## Test Helpers
89
116
 
@@ -84,7 +84,7 @@ const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
84
84
 
85
85
  ### Using `deploymentId: "latest"`
86
86
 
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.
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. For when to use this and how it fits with default run pinning, see [Versioning](/docs/foundations/versioning).
88
88
 
89
89
  ```typescript
90
90
  import { start } from "workflow/api";
@@ -96,7 +96,7 @@ const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
96
96
  ```
97
97
 
98
98
  <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.
99
+ 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.
100
100
  </Callout>
101
101
 
102
102
  <Callout type="warn">
@@ -27,6 +27,15 @@ const workflowConfig = {}
27
27
  export default withWorkflow(nextConfig, workflowConfig); // [!code highlight]
28
28
  ```
29
29
 
30
+ <Callout type="warn">
31
+ If a package in `serverExternalPackages` contains workflow code (`"use step"`,
32
+ `"use workflow"`, or serialization classes), `withWorkflow()` automatically
33
+ removes it from `serverExternalPackages` for the current build and prints a
34
+ warning. This ensures the package still gets transformed by the Workflow
35
+ compiler. Remove that package from `serverExternalPackages` in your
36
+ `next.config` to silence the warning.
37
+ </Callout>
38
+
30
39
  ### Monorepos and Workspace Imports
31
40
 
32
41
  By default, Next.js detects the correct workspace root automatically. If your Next.js app lives in a subdirectory such as `apps/web` and workspace resolution is not working correctly, you can set `outputFileTracingRoot` as a workaround:
@@ -78,6 +87,7 @@ The `workflows.local` options only affect local development. When deployed to Ve
78
87
 
79
88
  ## Exporting a Function
80
89
 
90
+
81
91
  If you are exporting a function in your `next.config` you will need to ensure you call the function returned from `withWorkflow`.
82
92
 
83
93
  ```typescript title="next.config.ts" lineNumbers
@@ -139,6 +139,8 @@ On Vercel, workflow runs are pegged to the deployment that started them. This me
139
139
 
140
140
  This ensures long-running workflows complete reliably without being affected by subsequent deployments.
141
141
 
142
+ For the full model, including rerunning on latest and explicit upgrade boundaries, see [Versioning](/docs/foundations/versioning).
143
+
142
144
  ## Security
143
145
 
144
146
  ### Consumer function security
@@ -35,4 +35,7 @@ Workflow programming can be a slight shift from how you traditionally write real
35
35
  <Card href="/docs/foundations/idempotency" title="Idempotency">
36
36
  Prevent duplicate side effects when retrying operations.
37
37
  </Card>
38
+ <Card href="/docs/foundations/versioning" title="Versioning">
39
+ Understand how runs stay pinned to deployments and when to opt in to newer code.
40
+ </Card>
38
41
  </Cards>
@@ -8,7 +8,8 @@
8
8
  "hooks",
9
9
  "streaming",
10
10
  "serialization",
11
- "idempotency"
11
+ "idempotency",
12
+ "versioning"
12
13
  ],
13
14
  "defaultOpen": true
14
15
  }
@@ -0,0 +1,280 @@
1
+ ---
2
+ title: Versioning
3
+ description: Understand how workflow runs are pinned to deployments, how to recover runs after a fix, and how to opt in to newer code explicitly.
4
+ type: guide
5
+ summary: Keep in-flight runs stable by default, then choose explicit upgrade boundaries when you need them.
6
+ prerequisites:
7
+ - /docs/foundations/starting-workflows
8
+ related:
9
+ - /docs/api-reference/workflow-api/start
10
+ - /cookbook/common-patterns/workflow-composition
11
+ ---
12
+
13
+ Workflow runs are pinned to the deployment that starts them. When a run begins, Workflow SDK records the deployment for that run and continues executing the run on that same copy of your code.
14
+
15
+ That default is intentional. Durable workflows can pause for minutes, days, or months. If the code underneath a paused run changed every time you deployed, an in-flight run could resume into a different function body, different step names, or different input types than the ones it started with. That can make type safety fragile and can break long-running work in hard-to-debug ways.
16
+
17
+ With Workflow SDK, you can keep shipping. New runs use new deployments, while existing runs keep the version they already understand.
18
+
19
+ ## Default behavior
20
+
21
+ Start a workflow normally:
22
+
23
+ ```typescript title="app/api/orders/route.ts" lineNumbers
24
+ import { start } from "workflow/api";
25
+ import { fulfillOrder } from "@/workflows/fulfill-order";
26
+
27
+ export async function POST(request: Request) {
28
+ const { orderId } = await request.json();
29
+
30
+ const run = await start(fulfillOrder, [orderId]); // [!code highlight]
31
+
32
+ return Response.json({ runId: run.runId });
33
+ }
34
+ ```
35
+
36
+ The run is tied to the deployment that handled this request. If you deploy a new version while the workflow is [sleeping](/docs/api-reference/workflow/sleep), [waiting on a hook](/docs/foundations/hooks), [retrying a step](/docs/foundations/errors-and-retries), or processing later queue messages, that existing run still resumes on the original deployment.
37
+
38
+ ```typescript title="workflows/fulfill-order.ts" lineNumbers
39
+ import { sleep } from "workflow";
40
+
41
+ export async function fulfillOrder(orderId: string) {
42
+ "use workflow";
43
+
44
+ await reserveInventory(orderId);
45
+ await sleep("2d");
46
+ await chargeCustomer(orderId);
47
+ await shipOrder(orderId);
48
+ }
49
+
50
+ async function reserveInventory(orderId: string) {
51
+ "use step";
52
+ // ...
53
+ }
54
+
55
+ async function chargeCustomer(orderId: string) {
56
+ "use step";
57
+ // ...
58
+ }
59
+
60
+ async function shipOrder(orderId: string) {
61
+ "use step";
62
+ // ...
63
+ }
64
+ ```
65
+
66
+ If you deploy a change to `chargeCustomer()` while a run is in the two-day sleep, the existing run does not suddenly resume into the new implementation. It continues on the deployment it started on. The next order starts on the latest deployment and uses the new code from the beginning.
67
+
68
+ ## Fixing in-flight runs
69
+
70
+ Sometimes you deploy because the old code had a bug. The safest fix is usually explicit:
71
+
72
+ 1. Deploy the fixed code.
73
+ 2. Find the affected runs in [observability](/docs/observability) or with the CLI.
74
+ 3. Cancel the old runs if they are still running.
75
+ 4. Rerun them on the latest deployment with the same inputs.
76
+
77
+ This keeps the version boundary visible. The old run ends as cancelled or failed, and the replacement run starts fresh on the fixed deployment. This is a good fit for one-off, ad-hoc upgrades where you explicitly opt in to moving affected runs onto a new version.
78
+
79
+ ```bash
80
+ # Inspect affected runs and copy the exact workflowName value.
81
+ npx workflow inspect runs \
82
+ --backend vercel \
83
+ --status running
84
+
85
+ # Cancel one run.
86
+ npx workflow cancel <run-id> \
87
+ --backend vercel
88
+
89
+ # Or bulk-cancel matching running runs.
90
+ npx workflow cancel \
91
+ --status running \
92
+ --workflowName "workflow//./workflows/fulfill-order//fulfillOrder" \
93
+ --backend vercel
94
+ ```
95
+
96
+ The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-api/world/observability) when you need display-friendly names.
97
+
98
+ In the [observability UI](/docs/observability), use **Rerun on latest** to enqueue the workflow again with the same inputs against the latest deployment.
99
+
100
+ If you are writing your own recovery route, call `start()` with the same arguments and `deploymentId: "latest"`:
101
+
102
+ ```typescript title="app/api/orders/rerun/route.ts" lineNumbers
103
+ import { start } from "workflow/api";
104
+ import { fulfillOrder } from "@/workflows/fulfill-order";
105
+
106
+ export async function POST(request: Request) {
107
+ const { orderId } = await request.json();
108
+
109
+ const run = await start(fulfillOrder, [orderId], {
110
+ deploymentId: "latest", // [!code highlight]
111
+ });
112
+
113
+ return Response.json({ runId: run.runId });
114
+ }
115
+ ```
116
+
117
+ <Callout type="warn">
118
+ `deploymentId: "latest"` 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. Because the caller and target deployment can be different, keep the [workflow function name and file path](/docs/errors/workflow-not-registered), arguments, and return value backward-compatible across the deployments you plan to bridge.
119
+ </Callout>
120
+
121
+ ## Self upgrading workflows
122
+
123
+ Some workflows are expected to run for a very long time. Scheduled loops, recurring jobs, agents, and chat sessions often should not stay on one deployment forever.
124
+
125
+ Model those as a sequence of runs. Each run does a bounded piece of work, then starts the next run on the latest deployment and exits. This is similar to `continueAsNew` in other durable execution systems, but in Workflow SDK it is just [explicit recursion through `start()`](/cookbook/common-patterns/workflow-composition).
126
+
127
+ ```typescript title="workflows/daily-digest.ts" lineNumbers
128
+ import { sleep } from "workflow";
129
+ import { start } from "workflow/api";
130
+
131
+ type DigestState = {
132
+ userId: string;
133
+ lastSentAt?: string;
134
+ };
135
+
136
+ export async function dailyDigest(state: DigestState) {
137
+ "use workflow";
138
+
139
+ const sentAt = await sendDigest(state.userId);
140
+ await sleep("1d");
141
+
142
+ const nextRunId = await continueDigest({
143
+ ...state,
144
+ lastSentAt: sentAt,
145
+ });
146
+
147
+ return { continuedAs: nextRunId };
148
+ }
149
+
150
+ async function continueDigest(state: DigestState) {
151
+ "use step";
152
+
153
+ const run = await start(dailyDigest, [state], {
154
+ deploymentId: "latest", // [!code highlight]
155
+ });
156
+
157
+ return run.runId;
158
+ }
159
+
160
+ async function sendDigest(userId: string) {
161
+ "use step";
162
+ // ...
163
+ return new Date().toISOString();
164
+ }
165
+ ```
166
+
167
+ This pattern gives every run a clear lifecycle:
168
+
169
+ - The current run stays on its original deployment.
170
+ - The next run starts on the latest deployment.
171
+ - The [serialized `state`](/docs/foundations/serialization) is the migration boundary between versions.
172
+ - Observability can link parent and child runs when a workflow starts another run.
173
+
174
+ ## Carrying context forward
175
+
176
+ Anything that is [serializable by Workflow SDK](/docs/foundations/serialization) can be passed from one run to the next as an argument. That includes plain state objects, `ReadableStream`, `WritableStream`, and other supported serialized values.
177
+
178
+ For example, a long export can register its [output stream](/docs/foundations/streaming) once, write progress from each run, and pass the same stream plus updated state into the next run:
179
+
180
+ ```typescript title="workflows/export-report.ts" lineNumbers
181
+ import { getWritable } from "workflow";
182
+ import { start } from "workflow/api";
183
+
184
+ type ExportState = {
185
+ exportId: string;
186
+ page: number;
187
+ };
188
+
189
+ export async function exportReport(
190
+ state: ExportState,
191
+ progress?: WritableStream<string>
192
+ ) {
193
+ "use workflow";
194
+
195
+ // Register the stream once. Continuation runs receive this same stream
196
+ // as an argument and keep writing to it.
197
+ const stream =
198
+ progress !== undefined ? progress : getWritable<string>();
199
+
200
+ const hasMore = await exportPage(state, stream);
201
+
202
+ if (!hasMore) {
203
+ await writeProgress(stream, { type: "done", totalPages: state.page });
204
+ return { totalPages: state.page };
205
+ }
206
+
207
+ const nextRunId = await continueExportOnLatest(
208
+ { ...state, page: state.page + 1 },
209
+ stream
210
+ );
211
+
212
+ return { continuedAs: nextRunId };
213
+ }
214
+
215
+ async function continueExportOnLatest(
216
+ state: ExportState,
217
+ stream: WritableStream<string>
218
+ ) {
219
+ "use step";
220
+
221
+ const run = await start(exportReport, [state, stream], {
222
+ deploymentId: "latest", // [!code highlight]
223
+ });
224
+
225
+ return run.runId;
226
+ }
227
+
228
+ async function exportPage(
229
+ state: ExportState,
230
+ stream: WritableStream<string>
231
+ ) {
232
+ "use step";
233
+
234
+ // Do work for this version boundary.
235
+ const hasMore = state.page < 10;
236
+ const writer = stream.getWriter();
237
+
238
+ try {
239
+ await writer.write(
240
+ JSON.stringify({ type: "page", page: state.page }) + "\n"
241
+ );
242
+ return hasMore;
243
+ } finally {
244
+ writer.releaseLock();
245
+ }
246
+ }
247
+
248
+ async function writeProgress(
249
+ stream: WritableStream<string>,
250
+ event: { type: "done"; totalPages: number }
251
+ ) {
252
+ "use step";
253
+
254
+ const writer = stream.getWriter();
255
+ try {
256
+ await writer.write(JSON.stringify(event) + "\n");
257
+ } finally {
258
+ writer.releaseLock();
259
+ }
260
+ }
261
+ ```
262
+
263
+ ```typescript title="app/api/export/route.ts" lineNumbers
264
+ import { start } from "workflow/api";
265
+ import { exportReport } from "@/workflows/export-report";
266
+
267
+ export async function POST(request: Request) {
268
+ const { exportId } = await request.json();
269
+
270
+ const run = await start(exportReport, [{ exportId, page: 1 }]);
271
+
272
+ // Linked continuation runs keep writing to the stream registered by
273
+ // the parent run, because that stream is passed forward as an argument.
274
+ return new Response(run.readable, {
275
+ headers: { "Content-Type": "application/jsonl" },
276
+ });
277
+ }
278
+ ```
279
+
280
+ Each run still has one clear version boundary: the current run stays on its original deployment, the next run starts on the latest deployment, and only the explicit state and stream handle are carried forward.
@@ -63,6 +63,12 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
63
63
  <span className="font-medium">SvelteKit</span>
64
64
  </div>
65
65
  </Card>
66
+ <Card href="/docs/getting-started/tanstack-start" >
67
+ <div className="flex flex-col items-center justify-center gap-2">
68
+ <TanStack className="size-16 dark:invert" />
69
+ <span className="font-medium">TanStack Start</span>
70
+ </div>
71
+ </Card>
66
72
  <Card className="opacity-50">
67
73
  <div className="flex flex-col items-center justify-center gap-2">
68
74
  <Nest className="size-16 dark:invert grayscale" />
@@ -70,11 +76,4 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
70
76
  <Badge variant="secondary">Coming soon</Badge>
71
77
  </div>
72
78
  </Card>
73
- <Card className="opacity-50">
74
- <div className="flex flex-col items-center justify-center gap-2">
75
- <TanStack className="size-16 dark:invert grayscale" />
76
- <span className="font-medium">TanStack Start</span>
77
- <Badge variant="secondary">Coming soon</Badge>
78
- </div>
79
- </Card>
80
79
  </Cards>
@@ -9,6 +9,7 @@
9
9
  "nitro",
10
10
  "nuxt",
11
11
  "sveltekit",
12
+ "tanstack-start",
12
13
  "vite"
13
14
  ],
14
15
  "defaultOpen": true
@@ -75,9 +75,9 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json
75
75
  </Accordion>
76
76
 
77
77
  <Accordion type="single" collapsible>
78
- <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
78
+ <AccordionItem value="configure-proxy-handler" className="[&_h3]:my-0">
79
79
  <AccordionTrigger className="text-sm">
80
- ### Configure Proxy Handler (if applicable)
80
+ <h3 id="configure-proxy-handler">Configure Proxy Handler (if applicable)</h3>
81
81
  </AccordionTrigger>
82
82
  <AccordionContent className="[&_p]:my-2">
83
83
 
@@ -85,7 +85,9 @@ If your Next.js app has a [proxy handler](https://nextjs.org/docs/app/api-refere
85
85
  (formerly known as "middleware"), you'll need to update the matcher pattern to exclude Workflow's
86
86
  internal paths to prevent the proxy handler from running on them.
87
87
 
88
- Add `.well-known/workflow/*` to your middleware's exclusion list:
88
+ If you see `[local world] Queue operation failed` with `Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer`, your proxy matcher is still intercepting Workflow's internal `POST /.well-known/workflow/v1/flow` request. This is especially easy to miss in Next.js 16, where `proxy.ts` replaced `middleware.ts`.
89
+
90
+ Add `.well-known/workflow/*` to your matcher exclusion list:
89
91
 
90
92
  ```typescript title="proxy.ts" lineNumbers
91
93
  import { NextResponse } from "next/server";
@@ -0,0 +1,241 @@
1
+ ---
2
+ title: TanStack Start
3
+ description: Set up your first durable workflow in a TanStack Start application.
4
+ type: guide
5
+ summary: Set up Workflow SDK in a TanStack Start app.
6
+ prerequisites:
7
+ - /docs/getting-started
8
+ related:
9
+ - /docs/foundations/workflows-and-steps
10
+ ---
11
+
12
+ This guide will walk through setting up your first workflow in a TanStack Start app. Along the way, you'll learn more about the concepts that are fundamental to using the Workflow SDK in your own projects.
13
+
14
+ ---
15
+
16
+ <Steps>
17
+
18
+ <Step>
19
+ ## Create Your TanStack Start Project
20
+
21
+ Start by creating a new TanStack Start project:
22
+
23
+ ```bash
24
+ npm create @tanstack/start@latest my-workflow-app
25
+ ```
26
+
27
+ Enter the newly made directory:
28
+
29
+ ```bash
30
+ cd my-workflow-app
31
+ ```
32
+
33
+ ### Install `workflow`
34
+
35
+ ```package-install
36
+ npm i workflow
37
+ ```
38
+
39
+ ### Configure TanStack Start
40
+
41
+ TanStack Start runs on Vite, so the Workflow SDK is wired in via the same `workflow/vite` plugin. Add `workflow()` to your Vite config to enable usage of the `"use workflow"` and `"use step"` directives.
42
+
43
+ ```typescript title="vite.config.ts" lineNumbers
44
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
45
+ import { defineConfig } from "vite";
46
+ import { workflow } from "workflow/vite";
47
+
48
+ export default defineConfig({
49
+ plugins: [
50
+ workflow(), // [!code highlight]
51
+ tanstackStart(),
52
+ ],
53
+ });
54
+ ```
55
+
56
+ <Accordion type="single" collapsible>
57
+ <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
58
+ <AccordionTrigger className="text-sm">
59
+ ### Setup IntelliSense for TypeScript (Optional)
60
+ </AccordionTrigger>
61
+ <AccordionContent className="[&_p]:my-2">
62
+
63
+ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json`:
64
+
65
+ ```json title="tsconfig.json" lineNumbers
66
+ {
67
+ "compilerOptions": {
68
+ // ... rest of your TypeScript config
69
+ "plugins": [
70
+ {
71
+ "name": "workflow" // [!code highlight]
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ ```
77
+
78
+ </AccordionContent>
79
+ </AccordionItem>
80
+ </Accordion>
81
+
82
+ </Step>
83
+
84
+ <Step>
85
+
86
+ ## Create Your First Workflow
87
+
88
+ Create a new file for our first workflow:
89
+
90
+ ```typescript title="src/workflows/user-signup.ts" lineNumbers
91
+ import { sleep } from "workflow";
92
+
93
+ export async function handleUserSignup(email: string) {
94
+ "use workflow"; // [!code highlight]
95
+
96
+ const user = await createUser(email);
97
+ await sendWelcomeEmail(user);
98
+
99
+ await sleep("5s"); // Pause for 5s - doesn't consume any resources
100
+ await sendOnboardingEmail(user);
101
+
102
+ return { userId: user.id, status: "onboarded" };
103
+ }
104
+ ```
105
+
106
+ We'll fill in those functions next, but let's take a look at this code:
107
+
108
+ * We define a **workflow** function with the directive `"use workflow"`. Think of the workflow function as the _orchestrator_ of individual **steps**.
109
+ * The Workflow SDK's `sleep` function allows us to suspend execution of the workflow without using up any resources. A sleep can be a few seconds, hours, days, or even months long.
110
+
111
+ ## Create Your Workflow Steps
112
+
113
+ Let's now define those missing functions.
114
+
115
+ ```typescript title="src/workflows/user-signup.ts" lineNumbers
116
+ import { FatalError } from "workflow"
117
+
118
+ // Our workflow function defined earlier
119
+
120
+ async function createUser(email: string) {
121
+ "use step"; // [!code highlight]
122
+
123
+ console.log(`Creating user with email: ${email}`);
124
+
125
+ // Full Node.js access - database calls, APIs, etc.
126
+ return { id: crypto.randomUUID(), email };
127
+ }
128
+
129
+ async function sendWelcomeEmail(user: { id: string; email: string; }) {
130
+ "use step"; // [!code highlight]
131
+
132
+ console.log(`Sending welcome email to user: ${user.id}`);
133
+
134
+ if (Math.random() < 0.3) {
135
+ // By default, steps will be retried for unhandled errors
136
+ throw new Error("Retryable!");
137
+ }
138
+ }
139
+
140
+ async function sendOnboardingEmail(user: { id: string; email: string}) {
141
+ "use step"; // [!code highlight]
142
+
143
+ if (!user.email.includes("@")) {
144
+ // To skip retrying, throw a FatalError instead
145
+ throw new FatalError("Invalid Email");
146
+ }
147
+
148
+ console.log(`Sending onboarding email to user: ${user.id}`);
149
+ }
150
+ ```
151
+
152
+ Taking a look at this code:
153
+
154
+ * Business logic lives inside **steps**. When a step is invoked inside a **workflow**, it gets enqueued to run on a separate request while the workflow is suspended, just like `sleep`.
155
+ * If a step throws an error, like in `sendWelcomeEmail`, the step will automatically be retried until it succeeds (or hits the step's max retry count).
156
+ * Steps can throw a `FatalError` if an error is intentional and should not be retried.
157
+
158
+ <Callout>
159
+ We'll dive deeper into workflows, steps, and other ways to suspend or handle events in [Foundations](/docs/foundations).
160
+ </Callout>
161
+
162
+ </Step>
163
+
164
+ <Step>
165
+
166
+ ## Create Your Route Handler
167
+
168
+ To invoke your new workflow, add a server handler at `src/routes/api/signup.ts`:
169
+
170
+ ```typescript title="src/routes/api/signup.ts"
171
+ import { createFileRoute } from "@tanstack/react-router";
172
+ import { json } from "@tanstack/react-start";
173
+ import { start } from "workflow/api";
174
+ import { handleUserSignup } from "../../workflows/user-signup";
175
+
176
+ export const Route = createFileRoute("/api/signup")({
177
+ server: {
178
+ handlers: {
179
+ POST: async ({ request }) => {
180
+ const { email } = await request.json();
181
+ // Executes asynchronously and doesn't block your app
182
+ await start(handleUserSignup, [email]);
183
+ return json({ message: "User signup workflow started" });
184
+ },
185
+ },
186
+ },
187
+ });
188
+ ```
189
+
190
+ This route handler creates a `POST` request endpoint at `/api/signup` that will trigger your workflow.
191
+
192
+ <Callout>
193
+ Workflows can be triggered from API routes or any server-side code.
194
+ </Callout>
195
+
196
+ </Step>
197
+
198
+ </Steps>
199
+
200
+ ## Run in development
201
+
202
+ To start your development server, run the following command in your terminal in the TanStack Start root directory:
203
+
204
+ ```bash
205
+ npm run dev
206
+ ```
207
+
208
+ Once your development server is running, you can trigger your workflow by running this command in the terminal:
209
+
210
+ ```bash
211
+ curl -X POST --json '{"email":"hello@example.com"}' http://localhost:3000/api/signup
212
+ ```
213
+
214
+ Check the dev server logs to see your workflow execute as well as the steps that are being processed.
215
+
216
+ Additionally, you can use the [Workflow SDK CLI or Web UI](/docs/observability) to inspect your workflow runs and steps in detail.
217
+
218
+ ```bash
219
+ # Open the observability Web UI
220
+ npx workflow web
221
+ # or if you prefer a terminal interface, use the CLI inspect command
222
+ npx workflow inspect runs
223
+ ```
224
+
225
+ ![Workflow SDK Web UI](/o11y-ui.png)
226
+
227
+ ---
228
+
229
+ ## Deploying to production
230
+
231
+ Workflow SDK apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
232
+
233
+ <FluidComputeCallout />
234
+
235
+ Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
236
+
237
+ ## Next Steps
238
+
239
+ * Learn more about the [Foundations](/docs/foundations).
240
+ * Check [Errors](/docs/errors) if you encounter issues.
241
+ * Explore the [API Reference](/docs/api-reference).
@@ -320,7 +320,7 @@ The compiler generates stable IDs for workflows and steps based on file paths an
320
320
  - **Portable**: Works across different runtimes and deployments
321
321
 
322
322
  <Callout type="info">
323
- Although IDs can change when files are moved or functions are renamed, Workflow SDK function assume atomic versioning in the World. This means changing IDs won't break old workflows from running, but will prevent run from being upgraded and will cause your workflow/step names to change in the observability across deployments.
323
+ Although IDs can change when files are moved or functions are renamed, Workflow SDK functions assume [atomic versioning](/docs/foundations/versioning) in the World. This means changing IDs won't break old workflows from running, but will prevent runs from being upgraded and will cause your workflow/step names to change in observability across deployments.
324
324
  </Callout>
325
325
 
326
326
  ## Framework Integration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "4.2.4",
3
+ "version": "4.2.5",
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": "4.0.4",
61
- "@workflow/cli": "4.2.4",
62
- "@workflow/core": "4.2.4",
63
- "@workflow/errors": "4.1.1",
60
+ "@workflow/astro": "4.0.5",
61
+ "@workflow/cli": "4.2.5",
62
+ "@workflow/core": "4.2.5",
63
+ "@workflow/errors": "4.1.2",
64
64
  "@workflow/typescript-plugin": "4.0.2",
65
- "@workflow/utils": "4.1.1",
66
- "@workflow/next": "4.0.5",
67
- "@workflow/nest": "0.0.4",
68
- "@workflow/nitro": "4.0.5",
69
- "@workflow/nuxt": "4.0.5",
70
- "@workflow/sveltekit": "4.0.4",
71
- "@workflow/rollup": "4.0.4"
65
+ "@workflow/utils": "4.1.2",
66
+ "@workflow/next": "4.0.6",
67
+ "@workflow/nest": "0.0.5",
68
+ "@workflow/nitro": "4.0.6",
69
+ "@workflow/nuxt": "4.0.6",
70
+ "@workflow/sveltekit": "4.0.5",
71
+ "@workflow/rollup": "4.0.5"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/ms": "2.1.0",