workflow 5.0.0-beta.0 → 5.0.0-beta.2

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 (86) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.d.ts +1 -3
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +2 -6
  5. package/dist/api.js +1 -1
  6. package/dist/astro.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/internal/builtins.js +1 -1
  9. package/dist/internal/class-serialization.js +1 -1
  10. package/dist/internal/errors.js +1 -1
  11. package/dist/nest.js +1 -1
  12. package/dist/next.cjs +4 -2
  13. package/dist/next.d.cts +1 -1
  14. package/dist/next.d.cts.map +1 -1
  15. package/dist/nitro.js +1 -1
  16. package/dist/nuxt.js +1 -1
  17. package/dist/observability.d.ts +1 -1
  18. package/dist/observability.js +2 -2
  19. package/dist/runtime.js +1 -1
  20. package/dist/stdlib.js +1 -1
  21. package/dist/sveltekit.js +1 -1
  22. package/dist/typescript-plugin.cjs +1 -1
  23. package/dist/vite.js +1 -1
  24. package/dist/workflow.js +1 -1
  25. package/docs/ai/resumable-streams.mdx +1 -1
  26. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  27. package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
  28. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  29. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  30. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  31. package/docs/api-reference/workflow-api/get-world.mdx +6 -6
  32. package/docs/api-reference/workflow-api/index.mdx +1 -1
  33. package/docs/api-reference/workflow-api/world/index.mdx +2 -2
  34. package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
  35. package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
  36. package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
  37. package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
  38. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  39. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  40. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  41. package/docs/cookbook/advanced/meta.json +10 -0
  42. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  43. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  44. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  45. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  46. package/docs/cookbook/agent-patterns/meta.json +10 -0
  47. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  48. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  49. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  50. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  51. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  52. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  53. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  54. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  55. package/docs/cookbook/common-patterns/meta.json +15 -0
  56. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  57. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  58. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  59. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  60. package/docs/cookbook/index.mdx +41 -0
  61. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  62. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  63. package/docs/cookbook/integrations/meta.json +4 -0
  64. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  65. package/docs/cookbook/meta.json +5 -0
  66. package/docs/deploying/building-a-world.mdx +45 -43
  67. package/docs/deploying/world/local-world.mdx +1 -1
  68. package/docs/deploying/world/postgres-world.mdx +10 -5
  69. package/docs/deploying/world/vercel-world.mdx +1 -1
  70. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  71. package/docs/getting-started/index.mdx +8 -1
  72. package/docs/getting-started/meta.json +2 -1
  73. package/docs/getting-started/next.mdx +24 -0
  74. package/docs/getting-started/python.mdx +165 -0
  75. package/docs/how-it-works/code-transform.mdx +6 -5
  76. package/docs/meta.json +1 -0
  77. package/docs/migration-guides/index.mdx +34 -0
  78. package/docs/migration-guides/meta.json +9 -0
  79. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  80. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  81. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  82. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  83. package/package.json +13 -14
  84. package/dist/internal/private.d.ts +0 -6
  85. package/dist/internal/private.d.ts.map +0 -1
  86. package/dist/internal/private.js +0 -6
@@ -2,26 +2,26 @@
2
2
  title: Streams
3
3
  description: Read, write, and manage real-time data streams for workflow runs.
4
4
  type: reference
5
- summary: "Methods: writeToStream(), writeToStreamMulti(), readFromStream(), closeStream(), listStreamsByRunId(), getStreamChunks(), getStreamInfo(). Stream methods live directly on the world object."
5
+ summary: "Methods: streams.write(), streams.writeMulti(), streams.get(), streams.close(), streams.list(), streams.getChunks(), streams.getInfo(). Stream methods live on world.streams."
6
6
  prerequisites:
7
7
  - /docs/api-reference/workflow-api/get-world
8
8
  related:
9
9
  - /docs/foundations/streaming
10
10
  - /docs/api-reference/workflow/get-writable
11
11
  keywords:
12
- - writeToStream
13
- - writeToStreamMulti
14
- - readFromStream
15
- - closeStream
16
- - listStreamsByRunId
17
- - getStreamChunks
18
- - getStreamInfo
12
+ - streams.write
13
+ - streams.writeMulti
14
+ - streams.get
15
+ - streams.close
16
+ - streams.list
17
+ - streams.getChunks
18
+ - streams.getInfo
19
19
  - Streamer interface
20
20
  - real-time streaming
21
21
  - stream lifecycle
22
22
  ---
23
23
 
24
- Stream methods live directly on the `world` object returned by `getWorld()`. Use them to write chunks, read streams, and manage stream lifecycle outside of the standard `getWritable()` pattern.
24
+ Stream methods live on `world.streams` (the `streams` sub-object of the `World` instance returned by `await getWorld()`). Use them to write chunks, read streams, and manage stream lifecycle outside of the standard `getWritable()` pattern.
25
25
 
26
26
  <Callout type="info">
27
27
  For most streaming use cases, use [`getWritable()`](/docs/api-reference/workflow/get-writable) inside steps. Direct stream methods are for advanced scenarios like building custom stream consumers or managing streams from outside a workflow.
@@ -32,82 +32,83 @@ Stream methods live directly on the `world` object returned by `getWorld()`. Use
32
32
  ```typescript lineNumbers
33
33
  import { getWorld } from "workflow/runtime";
34
34
 
35
- const world = getWorld(); // [!code highlight]
36
- // Stream methods are called directly on world — e.g. world.writeToStream()
35
+ const world = await getWorld(); // [!code highlight]
36
+ // Stream methods are called on world.streams — e.g. world.streams.write()
37
37
  ```
38
38
 
39
39
  ## Methods
40
40
 
41
- ### writeToStream()
41
+ ### write()
42
42
 
43
43
  Write a data chunk to a named stream.
44
44
 
45
45
  ```typescript lineNumbers
46
- await world.writeToStream("default", runId, chunk); // [!code highlight]
46
+ await world.streams.write(runId, "default", chunk); // [!code highlight]
47
47
  ```
48
48
 
49
49
  **Parameters:**
50
50
 
51
51
  | Parameter | Type | Description |
52
52
  |-----------|------|-------------|
53
- | `name` | `string` | The stream name |
54
53
  | `runId` | `string` | The workflow run ID |
54
+ | `name` | `string` | The stream name |
55
55
  | `chunk` | `string \| Uint8Array` | Data to write |
56
56
 
57
- ### writeToStreamMulti()
57
+ ### writeMulti()
58
58
 
59
- Write multiple chunks in a single operation. Optional optimization — not all World implementations support it. Falls back to sequential `writeToStream()` calls if unavailable.
59
+ Write multiple chunks in a single operation. Optional optimization — not all World implementations support it. Falls back to sequential `write()` calls if unavailable.
60
60
 
61
61
  ```typescript lineNumbers
62
- await world.writeToStreamMulti?.("default", runId, [chunk1, chunk2]); // [!code highlight]
62
+ await world.streams.writeMulti?.(runId, "default", [chunk1, chunk2]); // [!code highlight]
63
63
  ```
64
64
 
65
65
  **Parameters:**
66
66
 
67
67
  | Parameter | Type | Description |
68
68
  |-----------|------|-------------|
69
- | `name` | `string` | The stream name |
70
69
  | `runId` | `string` | The workflow run ID |
70
+ | `name` | `string` | The stream name |
71
71
  | `chunks` | `(string \| Uint8Array)[]` | Chunks to write, in order |
72
72
 
73
- ### readFromStream()
73
+ ### get()
74
74
 
75
75
  Read data from a named stream as a live `ReadableStream` that waits for new chunks in real time.
76
76
 
77
77
  ```typescript lineNumbers
78
- const readable = await world.readFromStream("default"); // [!code highlight]
78
+ const readable = await world.streams.get(runId, "default"); // [!code highlight]
79
79
  ```
80
80
 
81
81
  **Parameters:**
82
82
 
83
83
  | Parameter | Type | Description |
84
84
  |-----------|------|-------------|
85
+ | `runId` | `string` | The workflow run ID |
85
86
  | `name` | `string` | The stream name |
86
87
  | `startIndex` | `number` | Optional. Positive values skip chunks from the start (0-based). Negative values read from the tail (e.g. `-3` starts 3 chunks from the end). Clamped to 0. |
87
88
 
88
89
  **Returns:** `ReadableStream<Uint8Array>`
89
90
 
90
- ### closeStream()
91
+ ### close()
91
92
 
92
93
  Close a stream when done writing.
93
94
 
94
95
  ```typescript lineNumbers
95
- await world.closeStream("default", runId); // [!code highlight]
96
+ await world.streams.close(runId, "default"); // [!code highlight]
96
97
  ```
97
98
 
98
99
  **Parameters:**
99
100
 
100
101
  | Parameter | Type | Description |
101
102
  |-----------|------|-------------|
102
- | `name` | `string` | The stream name |
103
103
  | `runId` | `string` | The workflow run ID |
104
+ | `name` | `string` | The stream name |
104
105
 
105
- ### listStreamsByRunId()
106
+ ### list()
106
107
 
107
108
  List all stream names associated with a workflow run.
108
109
 
109
110
  ```typescript lineNumbers
110
- const streamNames = await world.listStreamsByRunId(runId); // [!code highlight]
111
+ const streamNames = await world.streams.list(runId); // [!code highlight]
111
112
  ```
112
113
 
113
114
  **Parameters:**
@@ -118,12 +119,12 @@ const streamNames = await world.listStreamsByRunId(runId); // [!code highlight]
118
119
 
119
120
  **Returns:** `string[]`
120
121
 
121
- ### getStreamChunks()
122
+ ### getChunks()
122
123
 
123
- Fetch stream chunks with cursor-based pagination. Unlike `readFromStream()` (which returns a live `ReadableStream`), this returns a snapshot of currently available chunks.
124
+ Fetch stream chunks with cursor-based pagination. Unlike `get()` (which returns a live `ReadableStream`), this returns a snapshot of currently available chunks.
124
125
 
125
126
  ```typescript lineNumbers
126
- const result = await world.getStreamChunks("default", runId, { // [!code highlight]
127
+ const result = await world.streams.getChunks(runId, "default", { // [!code highlight]
127
128
  limit: 50,
128
129
  }); // [!code highlight]
129
130
  // result.data: StreamChunk[], result.cursor, result.hasMore, result.done
@@ -133,8 +134,8 @@ const result = await world.getStreamChunks("default", runId, { // [!code highlig
133
134
 
134
135
  | Parameter | Type | Description |
135
136
  |-----------|------|-------------|
136
- | `name` | `string` | The stream name |
137
137
  | `runId` | `string` | The workflow run ID |
138
+ | `name` | `string` | The stream name |
138
139
  | `options.limit` | `number` | Max chunks per page (default: 100, max: 1000) |
139
140
  | `options.cursor` | `string` | Cursor from a previous response |
140
141
 
@@ -147,12 +148,12 @@ const result = await world.getStreamChunks("default", runId, { // [!code highlig
147
148
  | `hasMore` | `boolean` | Whether more pages of already-written chunks exist |
148
149
  | `done` | `boolean` | Whether the stream is fully closed. When `false`, new chunks may appear in future requests even after `hasMore` is `false`. |
149
150
 
150
- ### getStreamInfo()
151
+ ### getInfo()
151
152
 
152
153
  Retrieve lightweight metadata about a stream without fetching chunks.
153
154
 
154
155
  ```typescript lineNumbers
155
- const info = await world.getStreamInfo("default", runId); // [!code highlight]
156
+ const info = await world.streams.getInfo(runId, "default"); // [!code highlight]
156
157
  // info.tailIndex: last chunk index (-1 if empty), info.done: whether stream is closed
157
158
  ```
158
159
 
@@ -160,8 +161,8 @@ const info = await world.getStreamInfo("default", runId); // [!code highlight]
160
161
 
161
162
  | Parameter | Type | Description |
162
163
  |-----------|------|-------------|
163
- | `name` | `string` | The stream name |
164
164
  | `runId` | `string` | The workflow run ID |
165
+ | `name` | `string` | The stream name |
165
166
 
166
167
  **Returns:** `StreamInfoResponse`
167
168
 
@@ -181,8 +182,9 @@ import { getWorld } from "workflow/runtime";
181
182
  export async function GET(req: Request) {
182
183
  const url = new URL(req.url);
183
184
  const streamName = url.searchParams.get("name") ?? "default";
184
- const world = getWorld();
185
- const readable = await world.readFromStream(streamName); // [!code highlight]
185
+ const runId = url.searchParams.get("runId")!;
186
+ const world = await getWorld();
187
+ const readable = await world.streams.get(runId, streamName); // [!code highlight]
186
188
 
187
189
  return new Response(readable, {
188
190
  headers: { "Content-Type": "application/octet-stream" },
@@ -195,11 +197,11 @@ export async function GET(req: Request) {
195
197
  ```typescript lineNumbers
196
198
  import { getWorld } from "workflow/runtime";
197
199
 
198
- const world = getWorld();
200
+ const world = await getWorld();
199
201
  let cursor: string | undefined;
200
202
 
201
203
  do {
202
- const result = await world.getStreamChunks("default", runId, { cursor }); // [!code highlight]
204
+ const result = await world.streams.getChunks(runId, "default", { cursor }); // [!code highlight]
203
205
  for (const chunk of result.data) {
204
206
  console.log(`Chunk ${chunk.index}:`, chunk.data);
205
207
  }
@@ -0,0 +1,168 @@
1
+ ---
2
+ title: Custom Serialization
3
+ description: Make class instances serializable across workflow boundaries using the WORKFLOW_SERIALIZE and WORKFLOW_DESERIALIZE symbol protocol.
4
+ type: guide
5
+ summary: Implement the WORKFLOW_SERIALIZE and WORKFLOW_DESERIALIZE symbol protocol on classes so instances survive serialization when passed between workflow and step functions.
6
+ ---
7
+
8
+ <Callout>
9
+ This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
+ </Callout>
11
+
12
+ ## The Problem
13
+
14
+ Workflow functions run inside a sandboxed VM. Every value that crosses a function boundary — step arguments, step return values, workflow inputs — must be [serializable](/docs/foundations/serialization). Plain objects, strings, numbers, and many built-in types (`Date`, `Map`, `Set`, `RegExp`, etc.) work automatically, but **class instances** that don't implement the custom class serialization protocol will throw a serialization error.
15
+
16
+ ```typescript lineNumbers
17
+ class StorageClient {
18
+ constructor(private region: string) {}
19
+
20
+ async upload(key: string, body: Uint8Array) {
21
+ // ... uses this.region internally
22
+ }
23
+ }
24
+
25
+ export async function processFile(client: StorageClient) {
26
+ "use workflow";
27
+
28
+ // client fails to serialize — StorageClient doesn't implement custom class serialization
29
+ // The runtime throws a serialization error
30
+ await uploadStep(client, "output.json", data);
31
+ }
32
+ ```
33
+
34
+ Custom class serialization solves this by teaching the runtime how to convert your class instances to plain data and back.
35
+
36
+ ## The WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE Protocol
37
+
38
+ The `@workflow/serde` package exports two symbols that act as a custom class serialization protocol. When the workflow runtime encounters a class instance with these symbols, it knows how to convert it to plain data and back.
39
+
40
+ {/* @skip-typecheck - @workflow/serde is not mapped in the type-checker */}
41
+ ```typescript lineNumbers
42
+ import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde";
43
+
44
+ class Point {
45
+ constructor(public x: number, public y: number) {}
46
+
47
+ distanceTo(other: Point): number {
48
+ return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
49
+ }
50
+
51
+ static [WORKFLOW_SERIALIZE](instance: Point) { // [!code highlight]
52
+ return { x: instance.x, y: instance.y };
53
+ }
54
+
55
+ static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) { // [!code highlight]
56
+ return new Point(data.x, data.y);
57
+ }
58
+ }
59
+ ```
60
+
61
+ Both methods must be **static**. `WORKFLOW_SERIALIZE` receives an instance and returns plain serializable data. `WORKFLOW_DESERIALIZE` receives that same data and reconstructs a new instance.
62
+
63
+ <Callout type="warn">
64
+ Both serialization methods run inside the workflow VM. They must not use Node.js APIs, non-deterministic operations, or network calls. Keep them focused on extracting and reconstructing data.
65
+ </Callout>
66
+
67
+ ## Automatic Class Registration
68
+
69
+ For the runtime to deserialize a class, the class must be registered in a global registry with a stable `classId`. The SWC compiler plugin handles this automatically — when it detects a class with both `WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE` static methods, it generates registration code at build time.
70
+
71
+ This means you only need to implement the two symbol methods. The compiler assigns a deterministic `classId` based on the file path and class name, and registers it in the global `Symbol.for("workflow-class-registry")` registry.
72
+
73
+ <Callout type="info">
74
+ No manual registration is required for classes defined in your workflow files. The SWC plugin detects the serialization symbols and generates the registration automatically at build time.
75
+ </Callout>
76
+
77
+ ## Full Example: A Workflow-Safe Storage Client
78
+
79
+ Here's a complete example of a storage client class that survives serialization across workflow boundaries. This pattern is useful when you need an object with methods to be passed as a workflow input or returned from a step.
80
+
81
+ ```typescript lineNumbers
82
+ import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde";
83
+
84
+ interface StorageClientOptions {
85
+ region: string;
86
+ bucket: string;
87
+ accessKeyId?: string;
88
+ secretAccessKey?: string;
89
+ }
90
+
91
+ export class WorkflowStorageClient {
92
+ private readonly region: string;
93
+ private readonly bucket: string;
94
+ private readonly accessKeyId?: string;
95
+ private readonly secretAccessKey?: string;
96
+
97
+ constructor(options: StorageClientOptions) {
98
+ this.region = options.region;
99
+ this.bucket = options.bucket;
100
+ this.accessKeyId = options.accessKeyId;
101
+ this.secretAccessKey = options.secretAccessKey;
102
+ }
103
+
104
+ async upload(key: string, body: Uint8Array) {
105
+ "use step";
106
+ const { S3Client, PutObjectCommand } = await import("@aws-sdk/client-s3");
107
+ const client = new S3Client({
108
+ region: this.region,
109
+ credentials: this.accessKeyId
110
+ ? { accessKeyId: this.accessKeyId, secretAccessKey: this.secretAccessKey! }
111
+ : undefined,
112
+ });
113
+ await client.send(
114
+ new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: body })
115
+ );
116
+ }
117
+
118
+ async getSignedUrl(key: string): Promise<string> {
119
+ "use step";
120
+ const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
121
+ const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
122
+ const client = new S3Client({ region: this.region });
123
+ return getSignedUrl(client, new GetObjectCommand({ Bucket: this.bucket, Key: key }));
124
+ }
125
+
126
+ // --- Serialization protocol ---
127
+
128
+ static [WORKFLOW_SERIALIZE](instance: WorkflowStorageClient): StorageClientOptions { // [!code highlight]
129
+ return {
130
+ region: instance.region,
131
+ bucket: instance.bucket,
132
+ accessKeyId: instance.accessKeyId,
133
+ secretAccessKey: instance.secretAccessKey,
134
+ };
135
+ }
136
+
137
+ static [WORKFLOW_DESERIALIZE]( // [!code highlight]
138
+ data: StorageClientOptions
139
+ ): WorkflowStorageClient {
140
+ return new WorkflowStorageClient(data);
141
+ }
142
+ }
143
+ ```
144
+
145
+ Now this client can be passed into a workflow and used directly:
146
+
147
+ ```typescript lineNumbers
148
+ import { WorkflowStorageClient } from "./storage-client";
149
+
150
+ export async function processUpload(
151
+ client: WorkflowStorageClient,
152
+ data: Uint8Array
153
+ ) {
154
+ "use workflow";
155
+
156
+ // client is a real WorkflowStorageClient with working methods
157
+ await client.upload("output/result.json", data); // [!code highlight]
158
+ const url = await client.getSignedUrl("output/result.json"); // [!code highlight]
159
+ return { url };
160
+ }
161
+ ```
162
+
163
+ ## Key APIs
164
+
165
+ - [`WORKFLOW_SERIALIZE`](/docs/api-reference/workflow-serde/workflow-serialize) — symbol for the static serialization method
166
+ - [`WORKFLOW_DESERIALIZE`](/docs/api-reference/workflow-serde/workflow-deserialize) — symbol for the static deserialization method
167
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — marks a function for extraction and serialization
168
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
@@ -0,0 +1,148 @@
1
+ ---
2
+ title: Durable Objects
3
+ description: Model long-lived stateful entities as workflows that persist state across requests.
4
+ type: guide
5
+ summary: Build a durable counter or session object whose state survives restarts by using a workflow's event log as the persistence layer.
6
+ ---
7
+
8
+ <Callout>
9
+ This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
+ </Callout>
11
+
12
+ ## The Idea
13
+
14
+ A workflow's event log already records every step result and replays them to reconstruct state. This is the same property that makes an "object" durable — its fields survive cold starts, crashes, and redeployments. Instead of using a workflow to model a *process*, you can use one to model an *entity* with methods.
15
+
16
+ Each "method call" is a hook that the object's workflow loop awaits. External callers resume the hook with a payload describing the operation. The workflow applies the operation, updates its internal state, and waits for the next call.
17
+
18
+ ## Pattern: Durable Counter
19
+
20
+ A counter that persists its value without a database. Each increment/decrement is recorded in the event log.
21
+
22
+ ```typescript lineNumbers
23
+ import { defineHook, getWorkflowMetadata } from "workflow";
24
+ import { z } from "zod";
25
+
26
+ const counterAction = defineHook({ // [!code highlight]
27
+ schema: z.object({
28
+ type: z.enum(["increment", "decrement", "get"]),
29
+ amount: z.number().default(1),
30
+ }),
31
+ });
32
+
33
+ export async function durableCounter() {
34
+ "use workflow";
35
+
36
+ let count = 0;
37
+ const { workflowRunId } = getWorkflowMetadata();
38
+
39
+ while (true) {
40
+ const hook = counterAction.create({ token: `counter:${workflowRunId}` });
41
+ const action = await hook; // [!code highlight]
42
+
43
+ switch (action.type) {
44
+ case "increment":
45
+ count += action.amount;
46
+ await recordState(count);
47
+ break;
48
+ case "decrement":
49
+ count -= action.amount;
50
+ await recordState(count);
51
+ break;
52
+ case "get":
53
+ await emitValue(count);
54
+ break;
55
+ }
56
+ }
57
+ }
58
+
59
+ async function recordState(count: number) {
60
+ "use step";
61
+ // Step records the state transition in the event log.
62
+ // On replay, the step result restores `count` without re-executing.
63
+ return count;
64
+ }
65
+
66
+ async function emitValue(count: number) {
67
+ "use step";
68
+ return { count };
69
+ }
70
+ ```
71
+
72
+ ### Calling the Object
73
+
74
+ From an API route, resume the hook to "invoke a method" on the durable object:
75
+
76
+ ```typescript lineNumbers
77
+ import { resumeHook } from "workflow/api";
78
+
79
+ export async function POST(request: Request) {
80
+ const { runId, type, amount } = await request.json();
81
+ await resumeHook(`counter:${runId}`, { type, amount }); // [!code highlight]
82
+ return Response.json({ ok: true });
83
+ }
84
+ ```
85
+
86
+ ## Pattern: Durable Session
87
+
88
+ A chat session where conversation history is the durable state. Each user message is a hook event; the workflow accumulates messages and generates responses.
89
+
90
+ ```typescript lineNumbers
91
+ import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
92
+ import { DurableAgent } from "@workflow/ai/agent";
93
+ import { anthropic } from "@workflow/ai/anthropic";
94
+ import { z } from "zod";
95
+ import type { UIMessageChunk, ModelMessage } from "ai";
96
+
97
+ const messageHook = defineHook({ // [!code highlight]
98
+ schema: z.object({
99
+ role: z.literal("user"),
100
+ content: z.string(),
101
+ }),
102
+ });
103
+
104
+ export async function durableSession() {
105
+ "use workflow";
106
+
107
+ const writable = getWritable<UIMessageChunk>();
108
+ const { workflowRunId: runId } = getWorkflowMetadata();
109
+ const messages: ModelMessage[] = [];
110
+
111
+ const agent = new DurableAgent({
112
+ model: anthropic("claude-sonnet-4-20250514"),
113
+ instructions: "You are a helpful assistant.",
114
+ });
115
+
116
+ while (true) {
117
+ const hook = messageHook.create({ token: `session:${runId}` });
118
+ const userMessage = await hook; // [!code highlight]
119
+
120
+ messages.push({
121
+ role: userMessage.role,
122
+ content: userMessage.content,
123
+ });
124
+
125
+ await agent.stream({ messages, writable });
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## When to Use This
131
+
132
+ - **Entity-per-workflow**: Each user, document, or device gets its own workflow run. The run ID is the entity ID.
133
+ - **No external database needed**: State lives in the event log. Reads replay from the log; writes append to it.
134
+ - **Automatic consistency**: Only one execution runs at a time per workflow run, so there are no race conditions on the entity's state.
135
+
136
+ ## Trade-offs
137
+
138
+ - **Read latency**: Accessing current state requires replaying the event log (or caching the last known state in a step result).
139
+ - **Not a replacement for databases**: If you need to query across entities (e.g., "all counters above 100"), you still need a database. Durable objects are for single-entity state.
140
+ - **Log growth**: Long-lived objects accumulate large event logs. Consider periodic "snapshot" steps that checkpoint the full state.
141
+
142
+ ## Key APIs
143
+
144
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
145
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
146
+ - [`defineHook`](/docs/api-reference/workflow/define-hook) — type-safe hook for receiving external method calls
147
+ - [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
148
+ - [`resumeHook`](/docs/api-reference/workflow-api/resume-hook) — invoke a method on the durable object from an API route
@@ -0,0 +1,145 @@
1
+ ---
2
+ title: Isomorphic Packages
3
+ description: Publish reusable workflow packages that work both inside and outside the workflow runtime.
4
+ type: guide
5
+ summary: Use try/catch around getWorkflowMetadata, dynamic imports, and optional peer dependencies to build libraries that run in workflows and in plain Node.js.
6
+ ---
7
+
8
+ <Callout>
9
+ This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
10
+ </Callout>
11
+
12
+ ## The Challenge
13
+
14
+ If you're a library author publishing a package that integrates with workflow, your code needs to handle two environments:
15
+
16
+ 1. **Inside a workflow run** — `getWorkflowMetadata()` works, `"use step"` directives are transformed, and the full workflow runtime is available.
17
+ 2. **Outside a workflow** — your package is imported in a regular Node.js process, a test suite, or a project that doesn't use workflow at all.
18
+
19
+ A hard dependency on `workflow` will crash at import time for users who don't have it installed.
20
+
21
+ ## Pattern 1: Feature-Detect with `getWorkflowMetadata`
22
+
23
+ Use a try/catch to detect whether you're running inside a workflow. This lets you add durable behavior when available and fall back to standard execution otherwise.
24
+
25
+ ```typescript lineNumbers
26
+ import { getWorkflowMetadata } from "workflow";
27
+
28
+ export async function processPayment(amount: number, currency: string) {
29
+ "use workflow";
30
+
31
+ let runId: string | undefined;
32
+ try {
33
+ const metadata = getWorkflowMetadata(); // [!code highlight]
34
+ runId = metadata.workflowRunId;
35
+ } catch {
36
+ // Not running inside a workflow — proceed without durability
37
+ runId = undefined;
38
+ }
39
+
40
+ if (runId) {
41
+ // Inside a workflow: use the run ID as an idempotency key
42
+ return await chargeWithIdempotency(amount, currency, runId); // [!code highlight]
43
+ } else {
44
+ // Outside a workflow: standard charge
45
+ return await chargeStandard(amount, currency);
46
+ }
47
+ }
48
+
49
+ async function chargeWithIdempotency(amount: number, currency: string, idempotencyKey: string) {
50
+ "use step";
51
+ // Stripe charge with idempotency key from workflow run ID
52
+ return { charged: true, amount, currency, idempotencyKey };
53
+ }
54
+
55
+ async function chargeStandard(amount: number, currency: string) {
56
+ "use step";
57
+ return { charged: true, amount, currency };
58
+ }
59
+ ```
60
+
61
+ ## Pattern 2: Dynamic Imports
62
+
63
+ Avoid importing `workflow` at the top level. Use dynamic `import()` so the module is only loaded when actually needed.
64
+
65
+ ```typescript lineNumbers
66
+ export async function createDurableTask(name: string, payload: unknown) {
67
+ "use workflow";
68
+
69
+ let sleep: ((duration: string) => Promise<void>) | undefined;
70
+
71
+ try {
72
+ const wf = await import("workflow"); // [!code highlight]
73
+ sleep = wf.sleep;
74
+ } catch {
75
+ // workflow not installed — use setTimeout fallback
76
+ sleep = undefined;
77
+ }
78
+
79
+ await executeTask(name, payload);
80
+
81
+ if (sleep) {
82
+ // Inside workflow: durable sleep that survives restarts
83
+ await sleep("5m"); // [!code highlight]
84
+ } else {
85
+ // Outside workflow: plain timer (not durable)
86
+ await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
87
+ }
88
+
89
+ await sendNotification(name);
90
+ }
91
+
92
+ async function executeTask(name: string, payload: unknown) {
93
+ "use step";
94
+ return { executed: true, name, payload };
95
+ }
96
+
97
+ async function sendNotification(name: string) {
98
+ "use step";
99
+ return { notified: true, name };
100
+ }
101
+ ```
102
+
103
+ ## Pattern 3: Optional Peer Dependencies
104
+
105
+ In your `package.json`, declare `workflow` as an optional peer dependency. This signals to package managers that your library *can* use workflow but doesn't require it.
106
+
107
+ ```json
108
+ {
109
+ "name": "@acme/payments",
110
+ "peerDependencies": {
111
+ "workflow": ">=1.0.0"
112
+ },
113
+ "peerDependenciesMeta": {
114
+ "workflow": {
115
+ "optional": true
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ Then guard all workflow imports with dynamic `import()` and try/catch as shown above.
122
+
123
+ ## Real-World Examples
124
+
125
+ ### Mux AI
126
+
127
+ The Mux team published a reusable workflow package for video processing. Their library detects the workflow runtime and falls back to standard async processing when workflow isn't available.
128
+
129
+ ### World ID
130
+
131
+ World ID's identity verification library uses `getWorkflowMetadata()` to attach run IDs to their human-in-the-loop verification hooks, but the same library works in non-workflow environments for simple verification flows.
132
+
133
+ ## Guidelines for Library Authors
134
+
135
+ 1. **Never hard-import `workflow` at the top level** if your package should work without it.
136
+ 2. **Use `getWorkflowMetadata()` in a try/catch** as the canonical runtime detection pattern.
137
+ 3. **Mark `workflow` as an optional peer dependency** in `package.json`.
138
+ 4. **Test both paths**: run your test suite with and without the workflow runtime to catch import errors.
139
+ 5. **Document the dual behavior**: make it clear in your README which features require workflow and which work standalone.
140
+
141
+ ## Key APIs
142
+
143
+ - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
144
+ - [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
145
+ - [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — runtime detection and run ID access
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "Advanced",
3
+ "pages": [
4
+ "serializable-steps",
5
+ "durable-objects",
6
+ "isomorphic-packages",
7
+ "custom-serialization",
8
+ "publishing-libraries"
9
+ ]
10
+ }