workflow 5.0.0-beta.4 → 5.0.0-beta.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.
Files changed (35) hide show
  1. package/dist/api.d.ts +5 -1
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +14 -2
  4. package/dist/runtime.d.ts +1 -1
  5. package/dist/runtime.d.ts.map +1 -1
  6. package/dist/runtime.js +2 -2
  7. package/docs/api-reference/vitest/index.mdx +28 -1
  8. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  9. package/docs/api-reference/workflow-next/with-workflow.mdx +32 -0
  10. package/docs/changelog/eager-processing.mdx +595 -0
  11. package/docs/changelog/index.mdx +2 -1
  12. package/docs/cookbook/advanced/meta.json +1 -6
  13. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +29 -78
  14. package/docs/cookbook/common-patterns/timeouts.mdx +1 -1
  15. package/docs/cookbook/index.mdx +0 -1
  16. package/docs/deploying/world/postgres-world.mdx +5 -3
  17. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  18. package/docs/foundations/cancellation.mdx +460 -0
  19. package/docs/foundations/errors-and-retries.mdx +7 -3
  20. package/docs/foundations/meta.json +1 -0
  21. package/docs/foundations/serialization.mdx +77 -41
  22. package/docs/getting-started/astro.mdx +6 -0
  23. package/docs/getting-started/index.mdx +6 -7
  24. package/docs/getting-started/meta.json +1 -0
  25. package/docs/getting-started/nestjs.mdx +8 -0
  26. package/docs/getting-started/nitro.mdx +22 -0
  27. package/docs/getting-started/sveltekit.mdx +6 -0
  28. package/docs/getting-started/tanstack-start.mdx +241 -0
  29. package/docs/how-it-works/cancellation.mdx +287 -0
  30. package/docs/how-it-works/meta.json +2 -1
  31. package/docs/internal/index.mdx +19 -0
  32. package/docs/internal/meta.json +5 -0
  33. package/docs/internal/serializable-abort-controller.mdx +148 -0
  34. package/package.json +13 -12
  35. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
@@ -0,0 +1,460 @@
1
+ ---
2
+ title: Cancellation
3
+ description: Cancel long-running steps cooperatively using AbortSignal, or cancel entire workflow runs.
4
+ type: conceptual
5
+ summary: Cancel in-flight work with AbortSignal or stop entire workflow runs.
6
+ prerequisites:
7
+ - /docs/foundations/workflows-and-steps
8
+ related:
9
+ - /docs/foundations/common-patterns
10
+ - /docs/foundations/hooks
11
+ - /docs/how-it-works/cancellation
12
+ ---
13
+
14
+ Workflow DevKit supports two cancellation mechanisms: **AbortSignal** for fine-grained, cooperative cancellation of individual operations, and **run cancellation** for stopping an entire workflow. This guide covers both.
15
+
16
+ ## AbortSignal
17
+
18
+ `AbortController` and `AbortSignal` work across workflow and step boundaries. Create an `AbortController` with `new AbortController()` in a workflow function, pass its signal to steps, and call `abort()` — using the standard [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) API you already know.
19
+
20
+ ```typescript lineNumbers
21
+ import { sleep } from "workflow";
22
+
23
+ export async function cancellableWorkflow() {
24
+ "use workflow";
25
+
26
+ const controller = new AbortController(); // [!code highlight]
27
+
28
+ const result = await Promise.race([
29
+ longRunningStep(controller.signal), // [!code highlight]
30
+ sleep("30s").then(() => "timeout" as const),
31
+ ]);
32
+
33
+ if (result === "timeout") {
34
+ controller.abort(); // [!code highlight]
35
+ return { status: "timed out" };
36
+ }
37
+
38
+ return { status: "completed", result };
39
+ }
40
+
41
+ async function longRunningStep(signal: AbortSignal) {
42
+ "use step";
43
+
44
+ const response = await fetch("https://api.example.com/slow-operation", {
45
+ signal, // [!code highlight]
46
+ });
47
+
48
+ return response.json();
49
+ }
50
+ ```
51
+
52
+ No special imports, no wrapper functions — just the standard `AbortController` API.
53
+
54
+ <Callout type="info">
55
+ Cancellation is **cooperative**. Aborting a signal doesn't forcefully kill a step — it's up to the step's code to check `signal.aborted` or pass the signal to APIs like `fetch` that respect it. If a step ignores the signal, it runs to completion.
56
+ </Callout>
57
+
58
+ <Callout type="info">
59
+ To learn how `AbortController` works durably across workflow suspensions, replays, and step boundaries, see [How Cancellation Works](/docs/how-it-works/cancellation).
60
+ </Callout>
61
+
62
+ ### Timeout with Cancellation
63
+
64
+ Race a step against a timeout, and cancel the step if the timeout wins:
65
+
66
+ ```typescript lineNumbers
67
+ import { sleep } from "workflow";
68
+
69
+ export async function fetchWithTimeout(url: string) {
70
+ "use workflow";
71
+
72
+ const controller = new AbortController();
73
+
74
+ const result = await Promise.race([
75
+ fetchUrl(url, controller.signal),
76
+ sleep("10s").then(() => null),
77
+ ]);
78
+
79
+ if (result === null) {
80
+ controller.abort(); // [!code highlight]
81
+ throw new Error(`Request to ${url} timed out after 10s`);
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ async function fetchUrl(url: string, signal: AbortSignal) {
88
+ "use step";
89
+ const response = await fetch(url, { signal });
90
+ return response.json();
91
+ }
92
+ ```
93
+
94
+ ### Cancelling Parallel Work
95
+
96
+ When racing multiple steps, cancel the losers:
97
+
98
+ ```typescript lineNumbers
99
+ export async function firstResponder(urls: string[]) {
100
+ "use workflow";
101
+
102
+ const controller = new AbortController();
103
+
104
+ const result = await Promise.race( // [!code highlight]
105
+ urls.map((url) => fetchUrl(url, controller.signal)) // [!code highlight]
106
+ ); // [!code highlight]
107
+
108
+ controller.abort(); // Cancel remaining fetches // [!code highlight]
109
+
110
+ return result;
111
+ }
112
+
113
+ async function fetchUrl(url: string, signal: AbortSignal) {
114
+ "use step";
115
+ const response = await fetch(url, { signal });
116
+ return { url, data: await response.json() };
117
+ }
118
+ ```
119
+
120
+ ### Passing Signal Through a Pipeline
121
+
122
+ Pass the same signal to a chain of steps. Aborting cancels whichever step is currently running:
123
+
124
+ ```typescript lineNumbers
125
+ declare function splitIntoChunks(data: ArrayBuffer): ArrayBuffer[]; // @setup
126
+ declare function processChunk(chunk: ArrayBuffer): Promise<Uint8Array>; // @setup
127
+
128
+ export async function pipelineWorkflow(dataUrl: string) {
129
+ "use workflow";
130
+
131
+ const controller = new AbortController();
132
+
133
+ try {
134
+ const raw = await downloadData(dataUrl, controller.signal);
135
+ const transformed = await transformData(raw, controller.signal);
136
+ const result = await uploadData(transformed, controller.signal);
137
+ return result;
138
+ } catch (err) {
139
+ if (err instanceof Error && err.name === "AbortError") {
140
+ return { status: "cancelled" };
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+
146
+ async function downloadData(url: string, signal: AbortSignal) {
147
+ "use step";
148
+ const response = await fetch(url, { signal });
149
+ return response.arrayBuffer();
150
+ }
151
+
152
+ async function transformData(data: ArrayBuffer, signal: AbortSignal) {
153
+ "use step";
154
+
155
+ signal.throwIfAborted(); // [!code highlight]
156
+
157
+ const chunks = splitIntoChunks(data);
158
+ const results = [];
159
+
160
+ for (const chunk of chunks) {
161
+ signal.throwIfAborted(); // [!code highlight]
162
+ results.push(await processChunk(chunk));
163
+ }
164
+
165
+ return Buffer.concat(results);
166
+ }
167
+
168
+ async function uploadData(data: ArrayBuffer, signal: AbortSignal) {
169
+ "use step";
170
+ await fetch("https://storage.example.com/upload", {
171
+ method: "POST",
172
+ body: data,
173
+ signal,
174
+ });
175
+ return { status: "uploaded" };
176
+ }
177
+ ```
178
+
179
+ ### Step-Initiated Abort
180
+
181
+ A step can receive the full `AbortController` and call `abort()` to cancel parallel work. This is useful for watchdog/monitor patterns where one step observes an external condition and cancels other in-flight steps:
182
+
183
+ ```typescript lineNumbers
184
+ export async function processWithQuotaCheck(userId: string, dataUrl: string) {
185
+ "use workflow";
186
+
187
+ const controller = new AbortController();
188
+
189
+ // Run the work and a quota monitor in parallel
190
+ const [result] = await Promise.all([ // [!code highlight]
191
+ processData(dataUrl, controller.signal), // [!code highlight]
192
+ monitorQuota(userId, controller), // [!code highlight]
193
+ ]); // [!code highlight]
194
+
195
+ return result;
196
+ }
197
+
198
+ async function processData(url: string, signal: AbortSignal) {
199
+ "use step";
200
+ const response = await fetch(url, { signal });
201
+ const data = await response.arrayBuffer();
202
+ // ... expensive processing ...
203
+ return { processed: true };
204
+ }
205
+
206
+ async function monitorQuota(userId: string, controller: AbortController) {
207
+ "use step";
208
+
209
+ // Poll quota status while the other step is running
210
+ while (!controller.signal.aborted) {
211
+ const quota = await fetch(`https://api.example.com/quota/${userId}`);
212
+ const { exceeded } = await quota.json();
213
+
214
+ if (exceeded) {
215
+ controller.abort("Quota exceeded"); // Cancels processData // [!code highlight]
216
+ return;
217
+ }
218
+
219
+ await new Promise((resolve) => setTimeout(resolve, 5000));
220
+ }
221
+ }
222
+ ```
223
+
224
+ ### User-Triggered Cancellation with Hooks
225
+
226
+ Combine hooks with abort controllers to let users cancel in-flight work from an external API:
227
+
228
+ ```typescript lineNumbers
229
+ import { createHook } from "workflow";
230
+
231
+ export async function userCancellableWorkflow(jobId: string) {
232
+ "use workflow";
233
+
234
+ using cancelHook = createHook<{ reason: string }>({
235
+ token: `cancel:${jobId}`,
236
+ });
237
+
238
+ const controller = new AbortController();
239
+ const workPromise = doExpensiveWork(controller.signal);
240
+
241
+ const result = await Promise.race([ // [!code highlight]
242
+ workPromise.then((data) => ({ status: "completed", data })),
243
+ cancelHook.then((payload) => { // [!code highlight]
244
+ controller.abort(); // [!code highlight]
245
+ return { status: "cancelled", reason: payload.reason };
246
+ }),
247
+ ]);
248
+
249
+ return result;
250
+ }
251
+
252
+ async function doExpensiveWork(signal: AbortSignal) {
253
+ "use step";
254
+ const response = await fetch("https://api.example.com/expensive", { signal });
255
+ return response.json();
256
+ }
257
+ ```
258
+
259
+ ```typescript title="app/api/cancel/route.ts" lineNumbers
260
+ import { resumeHook } from "workflow/api";
261
+
262
+ export async function POST(request: Request) {
263
+ const { jobId, reason } = await request.json();
264
+
265
+ await resumeHook(`cancel:${jobId}`, { reason });
266
+ return Response.json({ cancelled: true });
267
+ }
268
+ ```
269
+
270
+ ### How Steps Handle Abort
271
+
272
+ When an `AbortSignal` is aborted, the behavior depends on how the step uses it:
273
+
274
+ | Usage | Behavior on Abort |
275
+ |-------|-------------------|
276
+ | `fetch(url, { signal })` | Request is cancelled, throws `AbortError` |
277
+ | `signal.throwIfAborted()` | Throws the abort reason |
278
+ | `signal.aborted` check | Returns `true`, step can exit gracefully |
279
+ | `signal.addEventListener('abort', fn)` | Callback fires, step can clean up |
280
+ | Ignored | Step runs to completion (abort is cooperative) |
281
+
282
+ ### Abort Errors Skip Retries
283
+
284
+ When a step throws due to an abort (e.g., `fetch` throws `AbortError`, or `signal.throwIfAborted()` throws), the error is automatically wrapped in a `FatalError`. This means the step **skips retries** and the error bubbles up to the workflow immediately.
285
+
286
+ This is the correct behavior because an abort is an intentional cancellation — retrying the step would just result in another abort. You don't need to manually wrap abort errors in `FatalError`.
287
+
288
+ ```typescript lineNumbers
289
+ import { sleep } from "workflow";
290
+
291
+ export async function workflow() {
292
+ "use workflow";
293
+ const controller = new AbortController();
294
+
295
+ try {
296
+ const result = await Promise.race([
297
+ cancellableStep(controller.signal),
298
+ sleep("5s").then(() => null),
299
+ ]);
300
+ if (result === null) controller.abort();
301
+ return result;
302
+ } catch (err) {
303
+ // AbortError arrives as FatalError — no retries attempted // [!code highlight]
304
+ return { status: "cancelled" };
305
+ }
306
+ }
307
+
308
+ async function cancellableStep(signal: AbortSignal) {
309
+ "use step";
310
+ // If this throws AbortError, it's automatically wrapped in FatalError
311
+ const response = await fetch("https://api.example.com/slow", { signal });
312
+ return response.json();
313
+ }
314
+ ```
315
+
316
+ ### Passing AbortSignal as Workflow Input
317
+
318
+ You can pass an `AbortSignal` from external code into a workflow via `start()`:
319
+
320
+ {/* @skip-typecheck: myWorkflow is not declared, this is a conceptual snippet */}
321
+ ```typescript lineNumbers
322
+ import { start } from "workflow/api";
323
+
324
+ export async function POST(request: Request) {
325
+ const controller = new AbortController();
326
+ const run = await start(myWorkflow, [controller.signal]); // [!code highlight]
327
+
328
+ // Later, cancel from external code
329
+ controller.abort(); // [!code highlight]
330
+ }
331
+ ```
332
+
333
+ When the signal is serialized at the `start()` boundary, an event listener is attached to the external signal that writes the cancellation packet to the backing stream. This means the external `abort()` propagates into the workflow — but only while the originating process is still alive (same constraint as passing a `ReadableStream` as input).
334
+
335
+ <Callout type="info">
336
+ For reliable external cancellation that works regardless of process lifetime, prefer the [User-Triggered Cancellation with Hooks](#user-triggered-cancellation-with-hooks) pattern. Hooks are durable and don't depend on the caller's process staying alive.
337
+ </Callout>
338
+
339
+ ## Run Cancellation
340
+
341
+ Run cancellation stops an entire workflow at the next suspension point. Unlike `AbortSignal`, it is not cooperative — the workflow does not continue executing after cancellation.
342
+
343
+ ```typescript title="app/api/cancel-run/route.ts" lineNumbers
344
+ import { getRun } from "workflow/api";
345
+
346
+ export async function POST(request: Request) {
347
+ const { runId } = await request.json();
348
+
349
+ const run = getRun(runId);
350
+ await run.cancel(); // [!code highlight]
351
+
352
+ return Response.json({ cancelled: true });
353
+ }
354
+ ```
355
+
356
+ <Callout type="info">
357
+ Calling `run.cancel()` is the same action as clicking the **Cancel** button on a run in the observability UI — both produce identical `run_cancelled` events in the event log.
358
+ </Callout>
359
+
360
+ When a run is cancelled:
361
+ - The workflow stops at its next suspension point (step call, hook await, or sleep)
362
+ - A `run_cancelled` event is recorded in the [event log](/docs/how-it-works/event-sourcing)
363
+ - All associated hooks are disposed and their tokens released
364
+ - Streams are closed
365
+
366
+ <Callout type="info">
367
+ Run cancellation does **not** automatically abort any outstanding `AbortSignal`s. Steps that are currently executing will run to completion. If you need in-flight cancellation of specific operations, use `AbortSignal`.
368
+ </Callout>
369
+
370
+ ## AbortSignal vs. Run Cancellation
371
+
372
+ | | AbortSignal | Run Cancellation |
373
+ |---|---|---|
374
+ | **Scope** | Individual operations within a step | Entire workflow run |
375
+ | **Triggered by** | Your code (`controller.abort()`) | External API (`run.cancel()`) |
376
+ | **Cooperative** | Yes — steps must check the signal | No — workflow stops at the next suspension point |
377
+ | **Granularity** | Can target specific steps or operations | All-or-nothing |
378
+ | **In-flight steps** | Aborted immediately if using the signal | Run to completion |
379
+
380
+ Use `AbortSignal` when you need fine-grained, in-flight cancellation of specific operations. Use run cancellation when you want to stop the entire workflow.
381
+
382
+ ## Best Practices
383
+
384
+ **Use `throwIfAborted()` before expensive work.** This throws the signal's abort reason if the signal is already aborted, preventing wasted compute:
385
+
386
+ ```typescript lineNumbers
387
+ async function expensiveStep(signal: AbortSignal) {
388
+ "use step";
389
+ signal.throwIfAborted(); // [!code highlight]
390
+ // ... expensive work ...
391
+ }
392
+ ```
393
+
394
+ **Handle abort errors in the workflow.** Abort errors arrive as `FatalError` (no retries) and can be caught with a standard try/catch:
395
+
396
+ ```typescript lineNumbers
397
+ declare function cancellableStep(signal: AbortSignal): Promise<void>; // @setup
398
+ import { FatalError } from "workflow";
399
+
400
+ export async function workflow() {
401
+ "use workflow";
402
+ const controller = new AbortController();
403
+
404
+ try {
405
+ await cancellableStep(controller.signal);
406
+ } catch (err) {
407
+ if (FatalError.is(err)) { // [!code highlight]
408
+ return { status: "cancelled" };
409
+ }
410
+ throw err;
411
+ }
412
+ }
413
+ ```
414
+
415
+ **Use `AbortSignal.any()` to combine signals:**
416
+
417
+ ```typescript lineNumbers
418
+ async function stepWithMultipleSignals(
419
+ userSignal: AbortSignal,
420
+ timeoutSignal: AbortSignal
421
+ ) {
422
+ "use step";
423
+
424
+ const combined = AbortSignal.any([userSignal, timeoutSignal]); // [!code highlight]
425
+ const response = await fetch("https://api.example.com/data", {
426
+ signal: combined,
427
+ });
428
+ return response.json();
429
+ }
430
+ ```
431
+
432
+ **Abort after a race:**
433
+
434
+ ```typescript lineNumbers
435
+ declare function stepA(signal: AbortSignal): Promise<string>; // @setup
436
+ declare function stepB(signal: AbortSignal): Promise<string>; // @setup
437
+
438
+ export async function workflow() {
439
+ "use workflow";
440
+ const controller = new AbortController();
441
+
442
+ const winner = await Promise.race([
443
+ stepA(controller.signal),
444
+ stepB(controller.signal),
445
+ ]);
446
+
447
+ controller.abort(); // Clean up whichever step is still running // [!code highlight]
448
+ return winner;
449
+ }
450
+ ```
451
+
452
+ This is safe even if both steps have already completed — aborting a finished operation is a no-op.
453
+
454
+ ## Related Documentation
455
+
456
+ - [How Cancellation Works](/docs/how-it-works/cancellation) — Hook and stream backing, serialization internals
457
+ - [Serialization](/docs/foundations/serialization) — Understanding serializable types
458
+ - [Common Patterns](/docs/foundations/common-patterns) — Timeout and race patterns
459
+ - [Hooks](/docs/foundations/hooks) — Pausing workflows for external events
460
+ - [Errors and Retries](/docs/foundations/errors-and-retries) — Handling step failures
@@ -141,7 +141,7 @@ callApi.maxRetries = 5; // Retry up to 5 times on failure (6 total attempts)
141
141
 
142
142
  ## Error Codes
143
143
 
144
- When a workflow run fails, the error may include a `code` that classifies the failure. You can access it programmatically via the `Run` class:
144
+ When a workflow run fails, the error includes an `errorCode` that classifies the failure, alongside the original thrown value (preserved as `cause`):
145
145
 
146
146
  ```typescript lineNumbers
147
147
  import { WorkflowRunFailedError } from "@workflow/errors";
@@ -153,8 +153,12 @@ try {
153
153
  const result = await run.returnValue;
154
154
  } catch (err) {
155
155
  if (WorkflowRunFailedError.is(err)) {
156
- console.log(err.cause.code); // "USER_ERROR", "RUNTIME_ERROR", or undefined
157
- console.log(err.cause.message); // The error message
156
+ console.log(err.errorCode); // "USER_ERROR", "RUNTIME_ERROR", or undefined
157
+ // `cause` is the original thrown value, hydrated through the workflow
158
+ // serialization pipeline. It can be any thrown value, so check shape.
159
+ if (err.cause instanceof Error) {
160
+ console.log(err.cause.message); // The error message
161
+ }
158
162
  }
159
163
  }
160
164
  ```
@@ -6,6 +6,7 @@
6
6
  "errors-and-retries",
7
7
  "hooks",
8
8
  "streaming",
9
+ "cancellation",
9
10
  "serialization",
10
11
  "idempotency"
11
12
  ],
@@ -55,6 +55,50 @@ These types have special handling and are explained in detail in the sections be
55
55
  - `Response`
56
56
  - `ReadableStream<Serializable>`
57
57
  - `WritableStream<Serializable>`
58
+ - `AbortController`
59
+ - `AbortSignal`
60
+
61
+ ## Pass-by-Value Semantics
62
+
63
+ **Parameters are passed by value, not by reference.** Steps receive deserialized copies of data. Mutations inside a step won't affect the original in the workflow.
64
+
65
+ **Incorrect:**
66
+
67
+ ```typescript title="workflows/incorrect-mutation.ts" lineNumbers
68
+ export async function updateUserWorkflow(userId: string) {
69
+ "use workflow";
70
+
71
+ let user = { id: userId, name: "John", email: "john@example.com" };
72
+ await updateUserStep(user);
73
+
74
+ // user.email is still "john@example.com" // [!code highlight]
75
+ console.log(user.email); // [!code highlight]
76
+ }
77
+
78
+ async function updateUserStep(user: { id: string; name: string; email: string }) {
79
+ "use step";
80
+ user.email = "newemail@example.com"; // Changes are lost // [!code highlight]
81
+ }
82
+ ```
83
+
84
+ **Correct - return the modified data:**
85
+
86
+ ```typescript title="workflows/correct-mutation.ts" lineNumbers
87
+ export async function updateUserWorkflow(userId: string) {
88
+ "use workflow";
89
+
90
+ let user = { id: userId, name: "John", email: "john@example.com" };
91
+ user = await updateUserStep(user); // Reassign the return value // [!code highlight]
92
+
93
+ console.log(user.email); // "newemail@example.com"
94
+ }
95
+
96
+ async function updateUserStep(user: { id: string; name: string; email: string }) {
97
+ "use step";
98
+ user.email = "newemail@example.com";
99
+ return user; // [!code highlight]
100
+ }
101
+ ```
58
102
 
59
103
  **Custom Classes:**
60
104
 
@@ -125,6 +169,39 @@ export async function fetch(...args: Parameters<typeof globalThis.fetch>) {
125
169
 
126
170
  This allows you to make HTTP requests directly in workflow functions while maintaining deterministic replay behavior through automatic caching.
127
171
 
172
+ ## AbortController & AbortSignal
173
+
174
+ `AbortController` and `AbortSignal` are serializable types that enable cooperative cancellation across workflow and step boundaries. Inside a workflow function, `new AbortController()` creates a durable controller that works across suspensions and step boundaries:
175
+
176
+ ```typescript lineNumbers
177
+ import { sleep } from "workflow";
178
+
179
+ export async function cancellableWorkflow() {
180
+ "use workflow";
181
+
182
+ const controller = new AbortController(); // [!code highlight]
183
+
184
+ const result = await Promise.race([
185
+ fetchData(controller.signal), // [!code highlight]
186
+ sleep("10s").then(() => null),
187
+ ]);
188
+
189
+ if (result === null) {
190
+ controller.abort(); // [!code highlight]
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ async function fetchData(signal: AbortSignal) {
197
+ "use step";
198
+ const response = await fetch("https://api.example.com/data", { signal });
199
+ return response.json();
200
+ }
201
+ ```
202
+
203
+ For usage patterns including timeouts, parallel cancellation, user-triggered cancellation, and run cancellation, see the [Cancellation Guide](/docs/foundations/cancellation). For details on the hook and stream backing that makes this work, see [How Cancellation Works](/docs/how-it-works/cancellation).
204
+
128
205
  ## Custom Class Serialization
129
206
 
130
207
  By default, custom class instances cannot be serialized because the serialization system doesn't know how to reconstruct them. You can make your classes serializable by implementing two static methods using special symbols from the `@workflow/serde` package.
@@ -332,44 +409,3 @@ export async function processOrderWorkflow() {
332
409
  }
333
410
  ```
334
411
 
335
- ## Pass-by-Value Semantics
336
-
337
- **Parameters are passed by value, not by reference.** Steps receive deserialized copies of data. Mutations inside a step won't affect the original in the workflow.
338
-
339
- **Incorrect:**
340
-
341
- ```typescript title="workflows/incorrect-mutation.ts" lineNumbers
342
- export async function updateUserWorkflow(userId: string) {
343
- "use workflow";
344
-
345
- let user = { id: userId, name: "John", email: "john@example.com" };
346
- await updateUserStep(user);
347
-
348
- // user.email is still "john@example.com" // [!code highlight]
349
- console.log(user.email); // [!code highlight]
350
- }
351
-
352
- async function updateUserStep(user: { id: string; name: string; email: string }) {
353
- "use step";
354
- user.email = "newemail@example.com"; // Changes are lost // [!code highlight]
355
- }
356
- ```
357
-
358
- **Correct - return the modified data:**
359
-
360
- ```typescript title="workflows/correct-mutation.ts" lineNumbers
361
- export async function updateUserWorkflow(userId: string) {
362
- "use workflow";
363
-
364
- let user = { id: userId, name: "John", email: "john@example.com" };
365
- user = await updateUserStep(user); // Reassign the return value // [!code highlight]
366
-
367
- console.log(user.email); // "newemail@example.com"
368
- }
369
-
370
- async function updateUserStep(user: { id: string; name: string; email: string }) {
371
- "use step";
372
- user.email = "newemail@example.com";
373
- return user; // [!code highlight]
374
- }
375
- ```
@@ -51,6 +51,12 @@ export default defineConfig({
51
51
  });
52
52
  ```
53
53
 
54
+ `workflow()` accepts an options object:
55
+
56
+ | Option | Type | Default | Description |
57
+ | --- | --- | --- | --- |
58
+ | `sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` | Controls source maps on generated workflow bundles. Accepts the same values as esbuild's `sourcemap` option. Set to `false` for smaller function bundles (useful for staying under the Vercel 250MB function size limit) at the cost of stack traces pointing at generated code. Can also be set via the `WORKFLOW_SOURCEMAP` environment variable. |
59
+
54
60
  <Accordion type="single" collapsible>
55
61
  <AccordionItem value="typescript-intellisense" className="[&_h3]:my-0">
56
62
  <AccordionTrigger className="text-sm">
@@ -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 href="/docs/getting-started/python">
67
73
  <div className="flex flex-col items-center justify-center gap-2">
68
74
  <Python className="size-16" />
@@ -77,11 +83,4 @@ import { Next, Nitro, SvelteKit, Nuxt, Hono, Bun, AstroDark, AstroLight, TanStac
77
83
  <Badge variant="secondary">Coming soon</Badge>
78
84
  </div>
79
85
  </Card>
80
- <Card className="opacity-50">
81
- <div className="flex flex-col items-center justify-center gap-2">
82
- <TanStack className="size-16 dark:invert grayscale" />
83
- <span className="font-medium">TanStack Start</span>
84
- <Badge variant="secondary">Coming soon</Badge>
85
- </div>
86
- </Card>
87
86
  </Cards>
@@ -9,6 +9,7 @@
9
9
  "nitro",
10
10
  "nuxt",
11
11
  "sveltekit",
12
+ "tanstack-start",
12
13
  "vite",
13
14
  "python"
14
15
  ],
@@ -386,6 +386,14 @@ WorkflowModule.forRoot({
386
386
  // Only used when moduleType is 'commonjs'
387
387
  // Should match the outDir in your tsconfig.json
388
388
  distDir: 'dist',
389
+
390
+ // Source maps on generated workflow bundles (default: 'inline').
391
+ // Accepts the same values as esbuild's sourcemap option: true, false,
392
+ // 'inline', 'linked', 'external', 'both'. Set to false for smaller
393
+ // function bundles (useful for staying under the Vercel 250MB function
394
+ // size limit) at the cost of stack traces pointing at generated code.
395
+ // Can also be set via the WORKFLOW_SOURCEMAP environment variable.
396
+ sourcemap: 'inline',
389
397
  });
390
398
  ```
391
399