workflow 5.0.0-beta.4 → 5.0.0-beta.6

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 (57) 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/ai/index.mdx +6 -5
  8. package/docs/api-reference/vitest/index.mdx +28 -1
  9. package/docs/api-reference/workflow-api/start.mdx +5 -4
  10. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  11. package/docs/api-reference/workflow-next/with-workflow.mdx +32 -0
  12. package/docs/changelog/eager-processing.mdx +595 -0
  13. package/docs/changelog/index.mdx +2 -1
  14. package/docs/cookbook/advanced/child-workflows.mdx +13 -24
  15. package/docs/cookbook/advanced/meta.json +1 -6
  16. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +29 -78
  17. package/docs/cookbook/agent-patterns/durable-agent.mdx +4 -0
  18. package/docs/cookbook/common-patterns/scheduling.mdx +4 -0
  19. package/docs/cookbook/common-patterns/timeouts.mdx +1 -1
  20. package/docs/cookbook/common-patterns/workflow-composition.mdx +7 -14
  21. package/docs/cookbook/index.mdx +0 -1
  22. package/docs/cookbook/integrations/ai-sdk.mdx +4 -0
  23. package/docs/cookbook/integrations/chat-sdk.mdx +4 -0
  24. package/docs/cookbook/integrations/sandbox.mdx +4 -0
  25. package/docs/deploying/building-a-world.mdx +1 -1
  26. package/docs/deploying/world/postgres-world.mdx +5 -3
  27. package/docs/deploying/world/vercel-world.mdx +2 -0
  28. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  29. package/docs/errors/hook-conflict.mdx +56 -4
  30. package/docs/foundations/cancellation.mdx +460 -0
  31. package/docs/foundations/errors-and-retries.mdx +7 -3
  32. package/docs/foundations/index.mdx +3 -0
  33. package/docs/foundations/meta.json +3 -1
  34. package/docs/foundations/serialization.mdx +77 -41
  35. package/docs/foundations/starting-workflows.mdx +5 -1
  36. package/docs/foundations/versioning.mdx +263 -0
  37. package/docs/getting-started/astro.mdx +6 -0
  38. package/docs/getting-started/index.mdx +6 -7
  39. package/docs/getting-started/meta.json +1 -0
  40. package/docs/getting-started/nestjs.mdx +8 -0
  41. package/docs/getting-started/next.mdx +5 -3
  42. package/docs/getting-started/nitro.mdx +22 -0
  43. package/docs/getting-started/sveltekit.mdx +6 -0
  44. package/docs/getting-started/tanstack-start.mdx +241 -0
  45. package/docs/how-it-works/cancellation.mdx +287 -0
  46. package/docs/how-it-works/code-transform.mdx +2 -2
  47. package/docs/how-it-works/event-sourcing.mdx +2 -2
  48. package/docs/how-it-works/meta.json +2 -1
  49. package/docs/internal/index.mdx +19 -0
  50. package/docs/internal/meta.json +5 -0
  51. package/docs/internal/serializable-abort-controller.mdx +148 -0
  52. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
  53. package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
  54. package/docs/migration-guides/migrating-from-temporal.mdx +5 -10
  55. package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
  56. package/package.json +13 -12
  57. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
@@ -50,7 +50,7 @@ Migration removes infrastructure and collapses indirection. Business logic stays
50
50
  | Signal | `createHook()` or `createWebhook()` | Use hooks for typed resume signals; webhooks for HTTP callbacks. |
51
51
  | Query | `getWritable({ namespace: 'status' })` stream | Durably stream status updates from the workflow. Clients read from the stream instead of polling a database. |
52
52
  | Update | `createHook()` + `resumeHook()` (one-way) | Temporal Updates return a value to the caller; hooks do not. If the Update returns data, either write the result to a named stream via `getWritable()` and have the caller read from it, or keep an HTTP read route that fetches the workflow's current state. |
53
- | Child Workflow | `"use step"` wrappers around `start()` / `getRun()` | Spawn from a step and return the `Run` object so observability can deep-link into child runs. |
53
+ | Child Workflow | `start()` plus a `"use step"` wrapper around `getRun()` | Spawn a child run and return the `Run` object so observability can deep-link into child runs. |
54
54
  | Activity retry policy | Step retries, `RetryableError`, `FatalError`, `maxRetries` | Retries live at the step boundary. |
55
55
  | Event History | Workflow event log / run timeline | Same durable replay; built-in observability UI replaces Temporal Web. Search attributes and visibility APIs have no direct equivalent — filter by run status and timestamps instead. |
56
56
 
@@ -173,19 +173,14 @@ Temporal Queries expose in-memory workflow state on demand. In the Workflow SDK,
173
173
 
174
174
  ### Minimal translation
175
175
 
176
- `start()` and `getRun()` are runtime APIs, so wrap them in `"use step"` functions. Return the `Run` object (not a plain `runId` string) so workflow observability can deep-link into child runs.
176
+ In v5, call `start()` directly from the workflow to spawn a child run. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to await the child result. Return the `Run` object (not a plain `runId` string) so workflow observability can deep-link into child runs.
177
177
 
178
178
  ```typescript title="workflow/workflows/parent.ts"
179
179
  import { start } from 'workflow/api';
180
180
 
181
- async function spawnChild(item: string) {
182
- 'use step'; // [!code highlight]
183
- return start(childWorkflow, [item]); // [!code highlight]
184
- }
185
-
186
181
  export async function parentWorkflow(item: string) {
187
182
  'use workflow';
188
- const child = await spawnChild(item); // [!code highlight]
183
+ const child = await start(childWorkflow, [item]); // [!code highlight]
189
184
  return { childRunId: child.runId };
190
185
  }
191
186
  ```
@@ -204,7 +199,7 @@ async function collectResult(runId: string) {
204
199
  }
205
200
  ```
206
201
 
207
- Call both steps from the parent in sequence: `const result = await collectResult(child.runId)`. To fan out, call `spawnChild` inside a loop, then `Promise.all` the `collectResult` calls.
202
+ Call `start()` and then `collectResult()` from the parent in sequence: `const result = await collectResult(child.runId)`. To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls.
208
203
 
209
204
  <Callout type="warn">
210
205
  Activity retry policy moves to the step boundary. Use `maxRetries`, `RetryableError`, and `FatalError` on each step instead of a single workflow-wide retry block.
@@ -308,7 +303,7 @@ Remove the Worker process, `@temporalio/*` dependencies, and the Temporal Server
308
303
  - Convert each Activity into a `"use step"` function.
309
304
  - Remove Worker and Task Queue code. Start workflows from the app with `start()`.
310
305
  - Replace Signals with `createHook()` or `createWebhook()` for HTTP callers.
311
- - Wrap `start()` and `getRun()` in `"use step"` functions for child workflows. Return the `Run` object from `start()` so observability can deep-link into child runs.
306
+ - Use `start()` directly for child workflows, and wrap `getRun()` in a `"use step"` function when collecting results. Return the `Run` object from `start()` so observability can deep-link into child runs.
312
307
  - Set retry policy per step with `maxRetries`, `RetryableError`, and `FatalError`.
313
308
  - Use `getStepMetadata().stepId` as the idempotency key for external side effects.
314
309
  - Stream status and progress from steps with `getWritable({ namespace: 'status' })`, and have clients read from the stream instead of polling.
@@ -48,7 +48,7 @@ Migration collapses the task abstraction into plain async functions. Business lo
48
48
  | `logger` / `metadata.set` | `console` + `getWritable({ namespace: 'status' })` | Logs flow through the run timeline. Status writes go on a named stream. |
49
49
  | `wait.for({ seconds \| minutes \| hours \| days })` / `wait.until({ date })` | `sleep()` | Import from `workflow`. |
50
50
  | `wait.forToken({ timeout })` | `createHook()` + `Promise.race` with `sleep()` | Hooks carry a typed token. |
51
- | `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` | Wrap both in `"use step"` functions. |
51
+ | `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` | Call `start()` directly; wrap `getRun()` collection in a `"use step"` function. |
52
52
  | `batch.triggerAndWait()` | `Promise.all(runIds.map(collectResult))` | Fan out via standard concurrency. |
53
53
  | `AbortTaskRunError` | `FatalError` | Stops retries immediately. |
54
54
  | `retry.onThrow` / `retry.fetch` | `RetryableError`, `FatalError`, `maxRetries` | Retry count lives on the step via `myStep.maxRetries = N` (default 3). Control delay between attempts by throwing `new RetryableError(msg, { retryAfter: '5s' })` — there is no built-in exponential helper; compute the delay yourself based on `getStepMetadata().attempt` if you need one. |
@@ -180,23 +180,14 @@ A hook is an inbound write channel. The caller that knows the token resumes the
180
180
 
181
181
  ## Spawn a child workflow
182
182
 
183
- `triggerAndWait()` splits into two steps: spawn and collect. `start()` and `getRun()` are runtime APIs, so wrap them in `"use step"` functions. Return the full `Run` object from `spawnChild` so observability tooling can deep-link to the child run.
183
+ `triggerAndWait()` splits into spawn and collect. In v5, call `start()` directly from the workflow to spawn the child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to collect the result.
184
184
 
185
185
  You can return either the full `Run` object (enables deep-linking) or just `run.runId` (simpler). The runtime serializes `Run` to its `runId` in the event log either way.
186
186
 
187
- ```typescript title="workflow/workflows/parent.ts"
188
- import { start } from 'workflow/api';
189
-
190
- async function spawnChild(item: string) {
191
- 'use step';
192
- return start(childWorkflow, [item]); // [!code highlight]
193
- }
194
- ```
195
-
196
- Await the result in a second step, then orchestrate both from the parent:
187
+ Await the result in a step, then orchestrate both from the parent:
197
188
 
198
189
  ```typescript title="workflow/workflows/parent.ts"
199
- import { getRun } from 'workflow/api';
190
+ import { getRun, start } from 'workflow/api';
200
191
 
201
192
  async function collectResult(runId: string) {
202
193
  'use step';
@@ -206,12 +197,12 @@ async function collectResult(runId: string) {
206
197
 
207
198
  export async function parentWorkflow(item: string) {
208
199
  'use workflow';
209
- const child = await spawnChild(item);
200
+ const child = await start(childWorkflow, [item]); // [!code highlight]
210
201
  return await collectResult(child.runId);
211
202
  }
212
203
  ```
213
204
 
214
- To fan out, call `spawnChild` inside a loop, then `Promise.all` the `collectResult` calls. That replaces `batch.triggerAndWait()`.
205
+ To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls. That replaces `batch.triggerAndWait()`.
215
206
 
216
207
  `Promise.all` rejects on first failure; use `Promise.allSettled` if you need batch-mode error tolerance similar to trigger.dev's `{ ok, output, error }` per-run result.
217
208
 
@@ -273,7 +264,7 @@ async function loadOrder(id: string) {
273
264
  - `wait.for({ seconds | minutes | hours | days })` / `wait.until({ date })` → `sleep('5m')` or `sleep(date)` from `workflow`.
274
265
  - `wait.forToken(token)` → `createHook({ token })` + `await`. Complete it with `resumeHook(token, payload)` from an API route.
275
266
  - `wait.forToken({ timeout })` → `Promise.race([hook, sleep(timeout)])`.
276
- - `triggerAndWait(payload)` → wrap `start(child, [payload])` in a `"use step"` function and return the `Run` object, then read the result with a second step that calls `getRun(runId).returnValue`.
267
+ - `triggerAndWait(payload)` → call `start(child, [payload])` from the workflow and return the `Run` object, then read the result with a step that calls `getRun(runId).returnValue`.
277
268
 
278
269
  ### Step 5: Start runs from the app
279
270
 
@@ -324,7 +315,7 @@ Throw `new RetryableError(msg, { retryAfter: '5s' })` to control delay between a
324
315
  - Swap `wait.for` / `wait.until` for `sleep()` from `workflow`.
325
316
  - Swap `wait.forToken` for `createHook()` (internal) or `createWebhook()` (HTTP).
326
317
  - Model `wait.forToken` timeouts as `Promise.race()` between the hook and `sleep()`.
327
- - Replace `triggerAndWait()` with `"use step"` wrappers around `start()` and `getRun()`.
318
+ - Replace `triggerAndWait()` with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
328
319
  - Replace `batch.triggerAndWait()` with `Promise.all` over the collected child `Run` handles.
329
320
  - Move `schemaTask` validation to the call site; pass typed arguments into the workflow.
330
321
  - Replace `AbortTaskRunError` with `FatalError`; model retries per step with `RetryableError` and `maxRetries`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.6",
4
4
  "description": "Workflow SDK - Build durable, resilient, and observable workflows",
5
5
  "main": "dist/typescript-plugin.cjs",
6
6
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  ".": {
30
30
  "types": "./dist/index.d.ts",
31
31
  "workflow": "./dist/workflow.js",
32
+ "node": "./dist/index.js",
32
33
  "require": "./dist/typescript-plugin.cjs",
33
34
  "default": "./dist/index.js"
34
35
  },
@@ -56,18 +57,18 @@
56
57
  },
57
58
  "dependencies": {
58
59
  "ms": "2.1.3",
59
- "@workflow/astro": "5.0.0-beta.4",
60
- "@workflow/cli": "5.0.0-beta.4",
61
- "@workflow/core": "5.0.0-beta.4",
62
- "@workflow/errors": "5.0.0-beta.1",
60
+ "@workflow/astro": "5.0.0-beta.6",
61
+ "@workflow/cli": "5.0.0-beta.6",
62
+ "@workflow/core": "5.0.0-beta.6",
63
+ "@workflow/errors": "5.0.0-beta.3",
63
64
  "@workflow/typescript-plugin": "5.0.0-beta.3",
64
- "@workflow/utils": "5.0.0-beta.1",
65
- "@workflow/next": "5.0.0-beta.4",
66
- "@workflow/nest": "5.0.0-beta.4",
67
- "@workflow/nuxt": "5.0.0-beta.4",
68
- "@workflow/sveltekit": "5.0.0-beta.4",
69
- "@workflow/rollup": "5.0.0-beta.4",
70
- "@workflow/nitro": "5.0.0-beta.4"
65
+ "@workflow/utils": "5.0.0-beta.2",
66
+ "@workflow/next": "5.0.0-beta.6",
67
+ "@workflow/nest": "5.0.0-beta.6",
68
+ "@workflow/nitro": "5.0.0-beta.6",
69
+ "@workflow/nuxt": "5.0.0-beta.6",
70
+ "@workflow/sveltekit": "5.0.0-beta.6",
71
+ "@workflow/rollup": "5.0.0-beta.6"
71
72
  },
72
73
  "devDependencies": {
73
74
  "@types/ms": "2.1.0",
@@ -1,318 +0,0 @@
1
- ---
2
- title: Distributed Abort Controller
3
- description: A distributed AbortController that uses durable workflows for cross-process cancellation signaling.
4
- type: guide
5
- summary: Build a distributed abort controller that uses workflow streams and hooks to propagate cancellation signals across process boundaries.
6
- ---
7
-
8
- Use this pattern when you need an `AbortController`-like interface that works across distributed systems. The controller uses a durable workflow to coordinate cancellation — calling `.abort()` on one machine triggers the `.signal` on any other machine.
9
-
10
- ## When to use this
11
-
12
- - **Cross-process cancellation** — Cancel a long-running operation from a different server, worker, or edge function
13
- - **Durable cancellation** — The abort signal persists even if the process that created it crashes
14
- - **UI stop buttons** — Let users cancel operations running on the server from the browser
15
- - **Timeout coordination** — The built-in TTL auto-expires stale controllers
16
-
17
- ## Pattern
18
-
19
- The `DistributedAbortController` class encapsulates a workflow that:
20
- 1. Accepts a user-provided unique ID (like a chat ID or task ID)
21
- 2. Creates or reconnects to an existing workflow using that ID
22
- 3. Waits for a hook signal OR TTL expiration
23
- 4. Writes a cancellation message to the run's stream when triggered
24
-
25
- ### Core Implementation
26
-
27
- ```typescript lineNumbers
28
- import { defineHook, getWritable, sleep } from "workflow";
29
- import { start, getRun, getHookByToken } from "workflow/api";
30
-
31
- // Default TTL: 24 hours
32
- const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
33
- // Default grace period: 1 hour (keeps hook alive after abort for late subscribers)
34
- const DEFAULT_GRACE_MS = 60 * 60 * 1000;
35
-
36
- // Hook to trigger the abort signal
37
- export const abortHook = defineHook<{ reason?: string }>();
38
-
39
- // The abort message written to the stream
40
- export type AbortMessage = {
41
- type: "abort";
42
- reason?: string;
43
- expired?: boolean;
44
- };
45
-
46
- // Helper to create a consistent hook token from the user ID
47
- function getAbortToken(id: string): string {
48
- return `abort:${id}`;
49
- }
50
-
51
- // Step function that writes the abort message to the stream
52
- async function writeAbortSignal(reason?: string, expired?: boolean) {
53
- "use step";
54
-
55
- const writable = getWritable<AbortMessage>();
56
- const writer = writable.getWriter();
57
- try {
58
- await writer.write({ type: "abort", reason, expired });
59
- } finally {
60
- writer.releaseLock();
61
- }
62
- await writable.close();
63
- }
64
-
65
- // Workflow that waits for abort or TTL expiration
66
- export async function abortControllerWorkflow(
67
- id: string,
68
- ttlMs: number,
69
- graceMs: number
70
- ) {
71
- "use workflow";
72
-
73
- const startTime = Date.now();
74
- const hook = abortHook.create({ token: getAbortToken(id) });
75
-
76
- // Race: manual abort OR TTL expiration // [!code highlight]
77
- const result = await Promise.race([
78
- hook.then((payload) => ({
79
- reason: payload.reason,
80
- expired: false,
81
- })),
82
- sleep(`${ttlMs}ms`).then(() => ({
83
- reason: "Controller expired",
84
- expired: true,
85
- })),
86
- ]);
87
-
88
- await writeAbortSignal(result.reason, result.expired);
89
-
90
- // Only sleep through grace period on TTL expiration (keeps hook alive for late subscribers). // [!code highlight]
91
- // Manual aborts complete immediately.
92
- if (result.expired) {
93
- const elapsed = Date.now() - startTime;
94
- const remainingTime = graceMs - (elapsed - ttlMs);
95
- if (remainingTime > 0) {
96
- await sleep(`${remainingTime}ms`); // [!code highlight]
97
- }
98
- }
99
-
100
- return { aborted: true, reason: result.reason, expired: result.expired };
101
- }
102
-
103
- /**
104
- * A distributed abort controller that works across process boundaries.
105
- * Uses a semantically meaningful ID (like a chat ID or task ID) to coordinate.
106
- */
107
- export class DistributedAbortController {
108
- private id: string;
109
- readonly runId: string;
110
-
111
- private constructor(id: string, runId: string) {
112
- this.id = id;
113
- this.runId = runId;
114
- }
115
-
116
- /**
117
- * Creates or reconnects to a distributed abort controller.
118
- * If a controller with this ID already exists, reconnects to it.
119
- * Otherwise, starts a new workflow.
120
- *
121
- * @param id - A unique, semantically meaningful ID (e.g., "chat:123")
122
- * @param options.ttlMs - Time-to-live in ms (default: 24 hours)
123
- * @param options.graceMs - Grace period after abort (default: 1 hour)
124
- */
125
- static async create( // [!code highlight]
126
- id: string,
127
- options: { ttlMs?: number; graceMs?: number } = {}
128
- ): Promise<DistributedAbortController> {
129
- const { ttlMs = DEFAULT_TTL_MS, graceMs = DEFAULT_GRACE_MS } = options;
130
- const token = getAbortToken(id);
131
-
132
- // Try to find an existing run with this hook token
133
- const existingHook = await getHookByToken(token).catch(() => null); // [!code highlight]
134
-
135
- if (existingHook) {
136
- // Reconnect to existing controller
137
- return new DistributedAbortController(id, existingHook.runId);
138
- }
139
-
140
- // Create a new workflow
141
- const run = await start(abortControllerWorkflow, [id, ttlMs, graceMs]); // [!code highlight]
142
- return new DistributedAbortController(id, run.runId);
143
- }
144
-
145
- /**
146
- * Triggers the abort signal.
147
- * Idempotent: safe to call multiple times or after the workflow has completed.
148
- */
149
- async abort(reason?: string): Promise<void> { // [!code highlight]
150
- try {
151
- await abortHook.resume(getAbortToken(this.id), { reason });
152
- } catch (error) {
153
- const msg = error instanceof Error ? error.message.toLowerCase() : '';
154
- if (msg.includes('not found') || msg.includes('expired')) {
155
- return;
156
- }
157
- throw error;
158
- }
159
- }
160
-
161
- /**
162
- * Returns an AbortSignal that fires when abort() is called or TTL expires.
163
- * The signal fires with a reason indicating what triggered it.
164
- */
165
- get signal(): AbortSignal { // [!code highlight]
166
- const run = getRun<{ aborted: boolean; reason?: string; expired?: boolean }>(this.runId);
167
- const controller = new AbortController();
168
- const readable = run.getReadable<AbortMessage>();
169
-
170
- (async () => {
171
- const reader = readable.getReader();
172
- try {
173
- while (true) {
174
- const { done, value } = await reader.read();
175
- if (done) break;
176
- if (value.type === "abort") {
177
- const reason = value.expired
178
- ? `${value.reason} (expired)`
179
- : value.reason;
180
- controller.abort(reason);
181
- break;
182
- }
183
- }
184
- } catch (error) {
185
- if (!controller.signal.aborted) {
186
- controller.abort(
187
- error instanceof Error ? error.message : "Stream read failed"
188
- );
189
- }
190
- } finally {
191
- reader.releaseLock();
192
- }
193
- })();
194
-
195
- return controller.signal;
196
- }
197
- }
198
- ```
199
-
200
- ### Usage: Single Process
201
-
202
- ```typescript lineNumbers
203
- import { DistributedAbortController } from "./distributed-abort-controller";
204
-
205
- // Create a controller with a meaningful ID
206
- const controller = await DistributedAbortController.create("chat:user-123");
207
-
208
- // Get the signal and use it with fetch
209
- const signal = controller.signal;
210
- const response = await fetch("https://api.example.com/long-operation", {
211
- signal,
212
- });
213
-
214
- // Later: abort the operation
215
- await controller.abort("User cancelled");
216
- ```
217
-
218
- ### Usage: Cross-Process Coordination
219
-
220
- ```typescript lineNumbers
221
- import { DistributedAbortController } from "./distributed-abort-controller";
222
-
223
- // Process A: Create the controller
224
- const controller = await DistributedAbortController.create("task:build-123");
225
- // start long operation using controller.signal...
226
-
227
- // Process B: Reconnect and abort (no run ID sharing needed!)
228
- const sameController = await DistributedAbortController.create("task:build-123"); // [!code highlight]
229
- await sameController.abort("Cancelled by admin");
230
-
231
- // Process C: Reconnect and listen
232
- const anotherRef = await DistributedAbortController.create("task:build-123");
233
- anotherRef.signal.addEventListener("abort", (e) => {
234
- console.log("Task was cancelled:", (e.target as AbortSignal).reason);
235
- });
236
- ```
237
-
238
- ### Custom TTL
239
-
240
- ```typescript lineNumbers
241
- import { DistributedAbortController } from "./distributed-abort-controller";
242
-
243
- // Short-lived controller for a quick operation (5 minutes)
244
- const shortLived = await DistributedAbortController.create("quick-task", {
245
- ttlMs: 5 * 60 * 1000,
246
- });
247
-
248
- // Long-lived controller for batch jobs (7 days)
249
- const longLived = await DistributedAbortController.create("batch-job", {
250
- ttlMs: 7 * 24 * 60 * 60 * 1000,
251
- });
252
-
253
- // When TTL expires, the signal fires with expired reason
254
- shortLived.signal.addEventListener("abort", (e) => {
255
- const reason = (e.target as AbortSignal).reason;
256
- if (reason?.includes("expired")) {
257
- console.log("Controller expired, cleaning up...");
258
- }
259
- });
260
- ```
261
-
262
- ### API Route for Remote Abort
263
-
264
- ```typescript lineNumbers
265
- import { DistributedAbortController } from "@/lib/distributed-abort-controller";
266
-
267
- export async function POST(
268
- request: Request,
269
- { params }: { params: Promise<{ id: string }> }
270
- ) {
271
- const { id } = await params;
272
- const { reason } = await request.json();
273
-
274
- const controller = await DistributedAbortController.create(id);
275
- await controller.abort(reason || "Cancelled via API");
276
-
277
- return Response.json({ success: true });
278
- }
279
- ```
280
-
281
- ### Client Cancel Button
282
-
283
- ```tsx lineNumbers
284
- "use client";
285
-
286
- export function CancelButton({ taskId }: { taskId: string }) {
287
- const handleCancel = async () => {
288
- await fetch(`/api/abort/${taskId}`, {
289
- method: "POST",
290
- headers: { "Content-Type": "application/json" },
291
- body: JSON.stringify({ reason: "User clicked cancel" }),
292
- });
293
- };
294
-
295
- return (
296
- <button type="button" onClick={handleCancel}>
297
- Cancel Operation
298
- </button>
299
- );
300
- }
301
- ```
302
-
303
- ## Tips
304
-
305
- - **Use semantic IDs** — Use meaningful IDs like `chat:123` or `task:abc` instead of random UUIDs
306
- - **Create is idempotent** — Calling `create()` with the same ID reconnects to the existing controller
307
- - **TTL auto-cleanup** — Workflows self-terminate after TTL expires; no manual cleanup needed
308
- - **Signal is a getter** — Each access to `.signal` creates a new listener; cache it if needed
309
- - **One-shot** — Once aborted or expired, the workflow completes; create a new controller for new operations
310
-
311
- ## Key APIs
312
-
313
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook for the abort trigger
314
- - [`getWritable()`](/docs/api-reference/workflow/get-writable) — write abort messages to the stream
315
- - [`sleep()`](/docs/api-reference/workflow/sleep) — TTL timer for auto-expiration
316
- - [`start()`](/docs/api-reference/workflow-api/start) — start the abort controller workflow
317
- - [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) — find existing run by hook token
318
- - [`getRun()`](/docs/api-reference/workflow-api/get-run) — reconnect to the workflow's readable stream