@workflow/core 4.0.1-beta.9 → 4.1.0-beta.51

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 (181) hide show
  1. package/dist/builtins.js +1 -1
  2. package/dist/class-serialization.d.ts +26 -0
  3. package/dist/class-serialization.d.ts.map +1 -0
  4. package/dist/class-serialization.js +66 -0
  5. package/dist/create-hook.js +1 -1
  6. package/dist/define-hook.d.ts +40 -25
  7. package/dist/define-hook.d.ts.map +1 -1
  8. package/dist/define-hook.js +22 -27
  9. package/dist/events-consumer.d.ts.map +1 -1
  10. package/dist/events-consumer.js +5 -1
  11. package/dist/flushable-stream.d.ts +82 -0
  12. package/dist/flushable-stream.d.ts.map +1 -0
  13. package/dist/flushable-stream.js +214 -0
  14. package/dist/global.d.ts +4 -1
  15. package/dist/global.d.ts.map +1 -1
  16. package/dist/global.js +21 -9
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -2
  20. package/dist/logger.js +1 -1
  21. package/dist/observability.d.ts +60 -0
  22. package/dist/observability.d.ts.map +1 -1
  23. package/dist/observability.js +265 -32
  24. package/dist/private.d.ts +10 -1
  25. package/dist/private.d.ts.map +1 -1
  26. package/dist/private.js +6 -1
  27. package/dist/runtime/helpers.d.ts +52 -0
  28. package/dist/runtime/helpers.d.ts.map +1 -0
  29. package/dist/runtime/helpers.js +264 -0
  30. package/dist/runtime/resume-hook.d.ts +17 -12
  31. package/dist/runtime/resume-hook.d.ts.map +1 -1
  32. package/dist/runtime/resume-hook.js +79 -64
  33. package/dist/runtime/start.d.ts +14 -0
  34. package/dist/runtime/start.d.ts.map +1 -1
  35. package/dist/runtime/start.js +71 -45
  36. package/dist/runtime/step-handler.d.ts +7 -0
  37. package/dist/runtime/step-handler.d.ts.map +1 -0
  38. package/dist/runtime/step-handler.js +337 -0
  39. package/dist/runtime/suspension-handler.d.ts +25 -0
  40. package/dist/runtime/suspension-handler.d.ts.map +1 -0
  41. package/dist/runtime/suspension-handler.js +182 -0
  42. package/dist/runtime/world.d.ts.map +1 -1
  43. package/dist/runtime/world.js +20 -21
  44. package/dist/runtime.d.ts +3 -7
  45. package/dist/runtime.d.ts.map +1 -1
  46. package/dist/runtime.js +103 -411
  47. package/dist/schemas.d.ts +1 -15
  48. package/dist/schemas.d.ts.map +1 -1
  49. package/dist/schemas.js +2 -15
  50. package/dist/serialization.d.ts +112 -21
  51. package/dist/serialization.d.ts.map +1 -1
  52. package/dist/serialization.js +469 -85
  53. package/dist/sleep.d.ts +10 -0
  54. package/dist/sleep.d.ts.map +1 -1
  55. package/dist/sleep.js +1 -1
  56. package/dist/source-map.d.ts +10 -0
  57. package/dist/source-map.d.ts.map +1 -0
  58. package/dist/source-map.js +56 -0
  59. package/dist/step/context-storage.d.ts +2 -1
  60. package/dist/step/context-storage.d.ts.map +1 -1
  61. package/dist/step/context-storage.js +1 -1
  62. package/dist/step/get-closure-vars.d.ts +9 -0
  63. package/dist/step/get-closure-vars.d.ts.map +1 -0
  64. package/dist/step/get-closure-vars.js +16 -0
  65. package/dist/step/get-step-metadata.js +1 -1
  66. package/dist/step/get-workflow-metadata.js +1 -1
  67. package/dist/step/writable-stream.d.ts +10 -2
  68. package/dist/step/writable-stream.d.ts.map +1 -1
  69. package/dist/step/writable-stream.js +6 -5
  70. package/dist/step.d.ts +1 -1
  71. package/dist/step.d.ts.map +1 -1
  72. package/dist/step.js +93 -47
  73. package/dist/symbols.d.ts +6 -0
  74. package/dist/symbols.d.ts.map +1 -1
  75. package/dist/symbols.js +7 -1
  76. package/dist/telemetry/semantic-conventions.d.ts +66 -38
  77. package/dist/telemetry/semantic-conventions.d.ts.map +1 -1
  78. package/dist/telemetry/semantic-conventions.js +16 -3
  79. package/dist/telemetry.d.ts +8 -4
  80. package/dist/telemetry.d.ts.map +1 -1
  81. package/dist/telemetry.js +39 -6
  82. package/dist/types.js +1 -1
  83. package/dist/util.d.ts +5 -24
  84. package/dist/util.d.ts.map +1 -1
  85. package/dist/util.js +19 -38
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.d.ts.map +1 -0
  88. package/dist/version.js +3 -0
  89. package/dist/vm/index.js +2 -2
  90. package/dist/vm/uuid.js +1 -1
  91. package/dist/workflow/create-hook.js +1 -1
  92. package/dist/workflow/define-hook.d.ts +3 -3
  93. package/dist/workflow/define-hook.d.ts.map +1 -1
  94. package/dist/workflow/define-hook.js +1 -1
  95. package/dist/workflow/get-workflow-metadata.js +1 -1
  96. package/dist/workflow/hook.d.ts.map +1 -1
  97. package/dist/workflow/hook.js +49 -14
  98. package/dist/workflow/index.d.ts +1 -1
  99. package/dist/workflow/index.d.ts.map +1 -1
  100. package/dist/workflow/index.js +2 -2
  101. package/dist/workflow/sleep.d.ts +1 -1
  102. package/dist/workflow/sleep.d.ts.map +1 -1
  103. package/dist/workflow/sleep.js +26 -39
  104. package/dist/workflow/writable-stream.d.ts +1 -1
  105. package/dist/workflow/writable-stream.d.ts.map +1 -1
  106. package/dist/workflow/writable-stream.js +1 -1
  107. package/dist/workflow.d.ts +1 -1
  108. package/dist/workflow.d.ts.map +1 -1
  109. package/dist/workflow.js +72 -9
  110. package/docs/api-reference/create-hook.mdx +133 -0
  111. package/docs/api-reference/create-webhook.mdx +225 -0
  112. package/docs/api-reference/define-hook.mdx +206 -0
  113. package/docs/api-reference/fatal-error.mdx +37 -0
  114. package/docs/api-reference/fetch.mdx +139 -0
  115. package/docs/api-reference/get-step-metadata.mdx +76 -0
  116. package/docs/api-reference/get-workflow-metadata.mdx +44 -0
  117. package/docs/api-reference/get-writable.mdx +292 -0
  118. package/docs/api-reference/index.mdx +55 -0
  119. package/docs/api-reference/meta.json +3 -0
  120. package/docs/api-reference/retryable-error.mdx +106 -0
  121. package/docs/api-reference/sleep.mdx +59 -0
  122. package/docs/foundations/common-patterns.mdx +253 -0
  123. package/docs/foundations/errors-and-retries.mdx +190 -0
  124. package/docs/foundations/hooks.mdx +455 -0
  125. package/docs/foundations/idempotency.mdx +55 -0
  126. package/docs/foundations/index.mdx +32 -0
  127. package/docs/foundations/meta.json +14 -0
  128. package/docs/foundations/serialization.mdx +157 -0
  129. package/docs/foundations/starting-workflows.mdx +211 -0
  130. package/docs/foundations/streaming.mdx +569 -0
  131. package/docs/foundations/workflows-and-steps.mdx +197 -0
  132. package/docs/how-it-works/code-transform.mdx +334 -0
  133. package/docs/how-it-works/event-sourcing.mdx +254 -0
  134. package/docs/how-it-works/framework-integrations.mdx +437 -0
  135. package/docs/how-it-works/meta.json +10 -0
  136. package/docs/how-it-works/understanding-directives.mdx +611 -0
  137. package/package.json +31 -25
  138. package/dist/builtins.js.map +0 -1
  139. package/dist/create-hook.js.map +0 -1
  140. package/dist/define-hook.js.map +0 -1
  141. package/dist/events-consumer.js.map +0 -1
  142. package/dist/global.js.map +0 -1
  143. package/dist/index.js.map +0 -1
  144. package/dist/logger.js.map +0 -1
  145. package/dist/observability.js.map +0 -1
  146. package/dist/parse-name.d.ts +0 -25
  147. package/dist/parse-name.d.ts.map +0 -1
  148. package/dist/parse-name.js +0 -40
  149. package/dist/parse-name.js.map +0 -1
  150. package/dist/private.js.map +0 -1
  151. package/dist/runtime/resume-hook.js.map +0 -1
  152. package/dist/runtime/start.js.map +0 -1
  153. package/dist/runtime/world.js.map +0 -1
  154. package/dist/runtime.js.map +0 -1
  155. package/dist/schemas.js.map +0 -1
  156. package/dist/serialization.js.map +0 -1
  157. package/dist/sleep.js.map +0 -1
  158. package/dist/step/context-storage.js.map +0 -1
  159. package/dist/step/get-step-metadata.js.map +0 -1
  160. package/dist/step/get-workflow-metadata.js.map +0 -1
  161. package/dist/step/writable-stream.js.map +0 -1
  162. package/dist/step.js.map +0 -1
  163. package/dist/symbols.js.map +0 -1
  164. package/dist/telemetry/semantic-conventions.js.map +0 -1
  165. package/dist/telemetry.js.map +0 -1
  166. package/dist/types.js.map +0 -1
  167. package/dist/util.js.map +0 -1
  168. package/dist/vm/index.js.map +0 -1
  169. package/dist/vm/uuid.js.map +0 -1
  170. package/dist/workflow/create-hook.js.map +0 -1
  171. package/dist/workflow/define-hook.js.map +0 -1
  172. package/dist/workflow/get-workflow-metadata.js.map +0 -1
  173. package/dist/workflow/hook.js.map +0 -1
  174. package/dist/workflow/index.js.map +0 -1
  175. package/dist/workflow/sleep.js.map +0 -1
  176. package/dist/workflow/writable-stream.js.map +0 -1
  177. package/dist/workflow.js.map +0 -1
  178. package/dist/writable-stream.d.ts +0 -23
  179. package/dist/writable-stream.d.ts.map +0 -1
  180. package/dist/writable-stream.js +0 -17
  181. package/dist/writable-stream.js.map +0 -1
@@ -0,0 +1,10 @@
1
+ {
2
+ "title": "How it works",
3
+ "pages": [
4
+ "understanding-directives",
5
+ "code-transform",
6
+ "framework-integrations",
7
+ "event-sourcing"
8
+ ],
9
+ "defaultOpen": false
10
+ }
@@ -0,0 +1,611 @@
1
+ ---
2
+ title: Understanding Directives
3
+ ---
4
+
5
+ import { File, Folder, Files } from "fumadocs-ui/components/files";
6
+
7
+ <Callout>
8
+ This guide explores how JavaScript directives enable the Workflow DevKit's execution model. For getting started with workflows, see the [getting started](/docs/getting-started) guides for your framework.
9
+ </Callout>
10
+
11
+ The Workflow Development Kit uses JavaScript directives (`"use workflow"` and `"use step"`) as the foundation for its durable execution model. Directives provide the compile-time semantic boundary necessary for workflows to suspend, resume, and maintain deterministic behavior across replays.
12
+
13
+ This page explores how directives enable this execution model and the design principles that led us here.
14
+
15
+ To understand how directives work, let's first understand what workflows and steps are in the Workflow DevKit.
16
+
17
+ ## Workflows and Steps Primer
18
+
19
+ The Workflow DevKit has two types of functions:
20
+
21
+ **Step functions** are side-effecting operations with full Node.js runtime access. Think of them like named RPC calls - they run once, their result is persisted, and they can be [retried on failure](/docs/foundations/errors-and-retries):
22
+
23
+ {/* @skip-typecheck: incomplete code sample */}
24
+ ```typescript lineNumbers
25
+ async function fetchUserData(userId: string) {
26
+ "use step";
27
+
28
+ // Full Node.js access: database calls, API requests, file I/O
29
+ const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
30
+ return user;
31
+ }
32
+ ```
33
+
34
+ **Workflow functions** are deterministic orchestrators that coordinate steps. They must be pure functions - during replay, the same step results always produce the same output. This is necessary because workflows resume by replaying their code from the beginning using cached step results; non-deterministic logic would break resumption. They run in a sandboxed environment without direct Node.js access:
35
+
36
+ ```typescript lineNumbers
37
+ export async function onboardUser(userId: string) {
38
+ "use workflow";
39
+
40
+ const user = await fetchUserData(userId); // Calls step
41
+
42
+ // Non-deterministic code would break replay behavior // [!code highlight]
43
+ if (Math.random() > 0.5) { // [!code highlight]
44
+ await sendWelcomeEmail(user); // [!code highlight]
45
+ } // [!code highlight]
46
+
47
+ return `Onboarded ${user.name}!`;
48
+ }
49
+ ```
50
+
51
+ **The key insight:** Workflows resume from suspension by replaying their code using cached step results from the [event log](/docs/how-it-works/event-sourcing). When a step like `await fetchUserData(userId)` is called:
52
+
53
+ - **If already executed:** Returns the cached result immediately from the event log
54
+ - **If not yet executed:** Suspends the workflow, enqueues the step for background execution, and resumes later with the result
55
+
56
+ This replay mechanism requires deterministic code. If `Math.random()` weren't seeded, the first execution might return `0.7` (sending the email) but replay might return `0.3` (skipping it), thus breaking resumption. The Workflow DevKit sandbox provides seeded `Math.random()` and `Date` to ensure consistent behavior across replays.
57
+
58
+ <Callout>
59
+ For a deeper dive into workflows and steps, see [Workflows and Steps](/docs/foundations/workflows-and-steps).
60
+ </Callout>
61
+
62
+ ## The Core Challenge
63
+
64
+ This execution model enables powerful durability features - workflows can suspend for days, survive restarts, and resume from any point. However, it also requires a semantic boundary in the code that tells **the compiler, runtime, and developer** that execution semantics have changed.
65
+
66
+ The challenge: how do we mark this boundary in a way that:
67
+
68
+ 1. Enables compile-time transformations and validation
69
+ 2. Prevents accidental use of non-deterministic APIs
70
+ 3. Allows static analysis of workflow structure
71
+ 4. Feels natural to JavaScript developers
72
+
73
+ Let's look at where directives have been used before, and the alternatives we considered:
74
+
75
+ ## Prior art on directives
76
+
77
+ JavaScript directives have precedent for changing execution semantics within a defined scope:
78
+
79
+ - `"use strict"` (introduced in ECMAScript 5 in 2009, TC39-standardized) changes language rules to make the runtime faster, safer, and more predictable.
80
+ - `"use client"` and `"use server"` (introduced by [React Server Components](https://react.dev/reference/rsc/server-components)) define an explicit boundary of "where" code gets executed - client-side browser JavaScript vs server-side Node.js.
81
+ - `"use workflow"` (introduced by the Workflow DevKit) defines both "where" code runs (in a deterministic sandbox environment) and "how" it runs (deterministic, resumable, sandboxed execution semantics).
82
+
83
+ Directives provide a build-time contract.
84
+
85
+ When the Workflow DevKit sees `"use workflow"`, it:
86
+
87
+ - Bundles the workflow and its dependencies into code that can be run in a sandbox
88
+ - Restricts access to Node.js APIs in that sandbox
89
+ - Enables future functionality and optimizations only possible with a build tool
90
+ - For instance, the bundled workflow code can be statically analyzed to generate UML diagrams/visualizations of the workflow
91
+
92
+ In addition to being important to the compiler, `"use workflow"` explicitly signals to the developer that you are entering a different execution mode.
93
+
94
+ <Callout type="info">
95
+ The `"use workflow"` directive is also used by the Language Server Plugin shipped with Workflow DevKit to provide IntelliSense to your IDE. Check the [getting started instructions](/docs/getting-started) for your framework for details on setting up the Language Server Plugin.
96
+ </Callout>
97
+
98
+ But we didn't get here immediately. This took some discovery to arrive at:
99
+
100
+ ## Alternatives We Explored
101
+
102
+ Before settling on directives, we prototyped several other approaches. Each had significant limitations that made them unsuitable for production use.
103
+
104
+ ### Runtime-Only "Suspense" API
105
+
106
+ Our first proof of concept used a wrapper-based API without a build step:
107
+
108
+ {/* @skip-typecheck: incomplete code sample */}
109
+ ```typescript lineNumbers
110
+ export const myWorkflow = workflow(() => {
111
+ const message = run(async () => step());
112
+ return `${message}!`;
113
+ });
114
+ ```
115
+
116
+ This implementation used "throwing promises" (similar to early React Suspense) to suspend execution. When a step needed to run, we'd throw a promise, catch it at the workflow boundary, execute the step, and replay the workflow with the result.
117
+
118
+ **The problems:**
119
+
120
+ **1. Every side effect needed wrapping**
121
+
122
+ Any operation that could produce non-deterministic results had to be wrapped in `run()`:
123
+
124
+ {/* @skip-typecheck: incomplete code sample */}
125
+ ```typescript lineNumbers
126
+ export const myWorkflow = workflow(async () => {
127
+ // These would be non-deterministic without wrapping
128
+ const now = await run(() => Date.now()); // [!code highlight]
129
+ const random = await run(() => Math.random()); // [!code highlight]
130
+ const user = await run(() => fetchUser()); // [!code highlight]
131
+
132
+ return { now, random, user };
133
+ });
134
+ ```
135
+
136
+ This was verbose and easy to forget. Moreover, if a developer forgot to wrap something innocent like using `Date.now()`, it led to unstable runtime behavior.
137
+
138
+ For example:
139
+
140
+ {/* @skip-typecheck: incomplete code sample */}
141
+ ```typescript lineNumbers
142
+ export const myWorkflow = workflow(async () => {
143
+ // Nothing stops you from doing this:
144
+ const now = Date.now(); // Non-deterministic, untracked! // [!code highlight]
145
+ const user = await run(() => fetchUser());
146
+
147
+ // This workflow would produce different results on replay // [!code highlight]
148
+ return { now, user };
149
+ });
150
+ ```
151
+
152
+ **2. Closures and mutation became unpredictable**
153
+
154
+ Variables captured in closures would behave unexpectedly when steps mutated them:
155
+
156
+ {/* @skip-typecheck: incomplete code sample */}
157
+ ```typescript lineNumbers
158
+ export const myWorkflow = workflow(async () => {
159
+ let counter = 0;
160
+
161
+ await run(() => {
162
+ counter++; // This mutation happens during step execution // [!code highlight]
163
+ return saveToDatabase(counter);
164
+ });
165
+
166
+ console.log(counter); // What is counter here? // [!code highlight]
167
+ // During execution: 1 (mutation preserved) // [!code highlight]
168
+ // During replay: 0 (mutation lost) // [!code highlight]
169
+ // Inconsistent behavior! // [!code highlight]
170
+ });
171
+ ```
172
+
173
+ The workflow function would replay multiple times, but mutations inside `run()` callbacks wouldn't persist across replays. This made reasoning about state nearly impossible.
174
+
175
+ **3. Error handling broke down**
176
+
177
+ Since we used thrown promises for control flow, `try/catch` blocks became unreliable:
178
+
179
+ {/* @skip-typecheck: incomplete code sample */}
180
+ ```typescript lineNumbers
181
+ export const myWorkflow = workflow(async () => {
182
+ try {
183
+ const result = await run(() => step());
184
+ return result;
185
+ } catch (error) { // [!code highlight]
186
+ // This could catch: // [!code highlight]
187
+ // 1. A real error from the step // [!code highlight]
188
+ // 2. The thrown promise used for suspension // [!code highlight]
189
+ // 3. An error during replay // [!code highlight]
190
+ // Hard to distinguish without special handling // [!code highlight]
191
+ console.error(error);
192
+ }
193
+ });
194
+ ```
195
+
196
+ ### Generator-Based API
197
+
198
+ We explored using generators for explicit suspension points, inspired by libraries like Effect.ts:
199
+
200
+ {/* @skip-typecheck: incomplete code sample */}
201
+ ```typescript lineNumbers
202
+ export const myWorkflow = workflow(function*() {
203
+ const message = yield* run(() => step());
204
+ return `${message}!`;
205
+ });
206
+ ```
207
+
208
+ <Callout type="info">
209
+ We're big fans of [Effect.ts](https://effect.website/) and the power of generator-based APIs for effect management. However, for workflow orchestration specifically, we found the syntax too heavy for developers unfamiliar with generators.
210
+ </Callout>
211
+
212
+ **The problems:**
213
+
214
+ **1. Syntax felt more like a DSL than JavaScript**
215
+
216
+ Generators require a custom mental model that differs significantly from familiar async/await patterns. The `yield*` syntax and generator delegation were unfamiliar to many developers:
217
+
218
+ {/* @skip-typecheck: incomplete code sample */}
219
+ ```typescript lineNumbers
220
+ // Standard async/await (familiar)
221
+ const result = await fetchData();
222
+
223
+ // Generator-based (unfamiliar)
224
+ const result = yield* run(() => fetchData()); // [!code highlight]
225
+ ```
226
+
227
+ Complex workflows became particularly verbose and difficult to read:
228
+
229
+ {/* @skip-typecheck: incomplete code sample */}
230
+ ```typescript lineNumbers
231
+ export const myWorkflow = workflow(function*() {
232
+ const user = yield* run(() => fetchUser());
233
+
234
+ // Can't use Promise.all directly - need sequential calls or custom helpers // [!code highlight]
235
+ const orders = yield* run(() => fetchOrders(user.id)); // [!code highlight]
236
+ const payments = yield* run(() => fetchPayments(user.id)); // [!code highlight]
237
+
238
+ // Or create a custom generator-aware parallel helper: // [!code highlight]
239
+ const [orders2, payments2] = yield* all([ // [!code highlight]
240
+ run(() => fetchOrders(user.id)), // [!code highlight]
241
+ run(() => fetchPayments(user.id)) // [!code highlight]
242
+ ]); // [!code highlight]
243
+
244
+ return { user, orders, payments };
245
+ });
246
+ ```
247
+
248
+ **2. Still no compile-time sandboxing**
249
+
250
+ Like the runtime-only approach, generators couldn't prevent non-deterministic code:
251
+
252
+ {/* @skip-typecheck: incomplete code sample */}
253
+ ```typescript lineNumbers
254
+ export const myWorkflow = workflow(function*() {
255
+ const now = Date.now(); // Still possible, still problematic // [!code highlight]
256
+ const user = yield* run(() => fetchUser());
257
+ return { now, user };
258
+ });
259
+ ```
260
+
261
+ The generator syntax addressed suspension but didn't solve the fundamental sandboxing problem.
262
+
263
+ ### File System-Based Conventions
264
+
265
+ We explored using file system conventions to identify workflows and steps, similar to how modern frameworks handle routing (Next.js, Hono, Nitro, SvelteKit):
266
+
267
+ <Files>
268
+ <Folder name="workflows" defaultOpen>
269
+ <File name="onboarding.ts" />
270
+ <File name="checkout.ts" />
271
+ </Folder>
272
+ <Folder name="steps" defaultOpen>
273
+ <File name="send-email.ts" />
274
+ <File name="charge-payment.ts" />
275
+ </Folder>
276
+ </Files>
277
+
278
+ With this approach, any function in the `workflows/` directory would be transformed as a workflow, and any function in `steps/` would be a step. No directives needed, just file locations.
279
+
280
+ **Why this could work:**
281
+
282
+ - Clear separation of concerns
283
+ - Enables compiler transformations based on file path
284
+ - Familiar pattern for developers used to file-based routing, for example Next.js
285
+
286
+ **Why we moved away:**
287
+
288
+ **1. Too opinionated for diverse ecosystems**
289
+
290
+ Different frameworks and developers have strong opinions about project structure. Forcing a specific directory layout often caused conflicts across various conventions, especially in existing codebases.
291
+
292
+ **2. No support for publishable, reusable functions**
293
+
294
+ We want developers to be able to publish libraries to npm that include step and workflow directives. Ideally, logic that is isomorphic so it could be used with and without Workflow DevKit. File system conventions made this impossible.
295
+
296
+ **3. Migration and code reuse became difficult**
297
+
298
+ Migrating existing code required moving files and restructuring projects rather than adding a single line.
299
+
300
+ The directive approach solved all these issues: it works in any project structure, supports code reuse and migration, enables npm packages, and allows functions to adapt to their execution context.
301
+
302
+ ### Decorators
303
+
304
+ We considered decorators, but they presented significant challenges both technical and ergonomic.
305
+
306
+ **Decorators are non-yet-standard and class-focused**
307
+
308
+ Decorators are not yet a standard syntax ([TC39 proposal](https://github.com/tc39/proposal-decorators)) and they currently only work with classes. A class decorator approach could look like this:
309
+
310
+ {/* @skip-typecheck: incomplete code sample */}
311
+ ```typescript lineNumbers
312
+ import {workflow, step} from "workflow";
313
+
314
+ class MyWorkflow {
315
+ @workflow() // [!code highlight]
316
+ static async processOrder(orderId: string) { // [!code highlight]
317
+ const order = await this.fetchOrder(orderId);
318
+ const payment = await this.processPayment(order);
319
+ return { orderId, payment };
320
+ }
321
+
322
+ @step() // [!code highlight]
323
+ static async fetchOrder(orderId: string) { // [!code highlight]
324
+ // ...
325
+ }
326
+ }
327
+ ```
328
+
329
+ This approach requires:
330
+
331
+ - Writing class boilerplate with static methods
332
+ - Storing/mutating class properties was not obvious (similar closure/mutation issues as the runtime-only approach)
333
+ - Class-based syntax that doesn't feel "JavaScript native" to developers used to functional patterns
334
+
335
+ As the JavaScript ecosystem has moved toward function-forward programming (exemplified by React's shift from class components to functions and hooks), requiring developers to use classes felt like a step backward and also didn't match our own personal taste as authors of the DevKit.
336
+
337
+ **The core problem: Presents workflows as regular runtime code**
338
+
339
+ While decorators can be handled at compile-time with build tool support, they present workflow functions as if they were regular, composable JavaScript code, when they're actually compile-time declarations that need special handling.
340
+
341
+ <Callout>
342
+ See the [Macro Wrapper](#macro-wrapper-approach) section below for a deeper dive into why this approach breaks down with concrete examples.
343
+ </Callout>
344
+
345
+ ### Macro Wrapper Approach
346
+
347
+ We also explored compile-time macro approaches - using a compiler to transform wrapper functions or decorators into directive-based code:
348
+
349
+ {/* @skip-typecheck: incomplete code sample */}
350
+ ```typescript lineNumbers
351
+ // Function wrapper approach
352
+ import { useWorkflow } from "workflow"
353
+
354
+ export const processOrder = useWorkflow(async (orderId: string) => { // [!code highlight]
355
+ const order = await fetchOrder(orderId);
356
+ return { orderId };
357
+ });
358
+
359
+ // Decorator approach (would work similarly)
360
+ class MyWorkflow {
361
+ @workflow() // [!code highlight]
362
+ static async processOrder(orderId: string) {
363
+ const order = await fetchOrder(orderId);
364
+ return { orderId };
365
+ }
366
+
367
+ // ...
368
+ }
369
+ ```
370
+
371
+ The compiler could transform both to be equivalent to WDK's directive approach:
372
+
373
+ ```typescript lineNumbers
374
+ export const processOrder = async (orderId: string) => {
375
+ "use workflow"; // [!code highlight]
376
+ const order = await fetchOrder(orderId);
377
+ return { orderId };
378
+ };
379
+ ```
380
+
381
+ The benefit is that macros could enforce types and provide "Go To Definition" or other LSP features out of the box.
382
+
383
+ However, **the core problem remains: Workflows aren't runtime values**
384
+
385
+ The fundamental issue is that both wrappers and decorators make workflows appear to be **first-class, runtime values** when they're actually **compile-time declarations**. This mismatch between syntax and semantics creates numerous failure modes.
386
+
387
+ **Concrete examples of how this breaks:**
388
+
389
+ {/* @skip-typecheck: incomplete code sample */}
390
+ ```typescript lineNumbers
391
+ // Someone writes a "helpful" utility
392
+ function withRetry(fn: Function) {
393
+ return useWorkflow(async (...args) => { // Works with useWorkflow // [!code highlight]
394
+ try {
395
+ return await fn(...args);
396
+ } catch (error) {
397
+ return await fn(...args); // Retry once
398
+ }
399
+ });
400
+ }
401
+
402
+ // Note: the same utility would be written similarly for a decorator based syntax
403
+
404
+ // Usage looks innocent in both cases
405
+ export const processOrder = withRetry(async (orderId: string) => { // [!code highlight]
406
+ // Is this deterministic? Can it call steps?
407
+ // Nothing in this function indicates the developer is in the
408
+ // deterministic sandboxed workflow
409
+ // Also where is the retry happening? inside or outside the workflow?
410
+ const order = await fetchOrder(orderId);
411
+ return order;
412
+ });
413
+ ```
414
+
415
+ The developer writing `processOrder` has no visible signal that they're in a deterministic, sandboxed environment. It's also ambiguous whether the retry logic executes inside the workflow or outside, and the actual behavior likely doesn't match developer intuition.
416
+
417
+ **Why the compiler can't catch this:**
418
+
419
+ To detect that `processOrder` is actually a workflow, the compiler would need whole-program analysis to track that:
420
+
421
+ 1. `withRetry` returns the result of `useWorkflow`
422
+ 2. Therefore `processOrder = withRetry(...)` is a workflow
423
+ 3. The function passed to `withRetry` will execute in a sandboxed context
424
+
425
+ This level of cross-function analysis is impractical for build tools - it would require analyzing every function call chain in your entire codebase and all dependencies. The compiler can only reliably detect direct `useWorkflow` calls, not calls hidden behind abstractions.
426
+
427
+ ## How Directives Solve These Problems
428
+
429
+ Directives address all the issues we encountered with previous approaches:
430
+
431
+ **1. Compile-time semantic boundary**
432
+
433
+ The `"use workflow"` directive tells the compiler to treat this code differently:
434
+
435
+ ```typescript lineNumbers
436
+ export async function processOrder(orderId: string) {
437
+ "use workflow"; // Compiler knows: transform this for sandbox execution // [!code highlight]
438
+
439
+ const order = await fetchOrder(orderId); // Compiler knows: this is a step call // [!code highlight]
440
+ return { orderId, order };
441
+ }
442
+ ```
443
+
444
+ **2. Build-time validation**
445
+
446
+ The compiler can enforce restrictions before deployment:
447
+
448
+ ```typescript lineNumbers
449
+ export async function badWorkflow() {
450
+ "use workflow";
451
+
452
+ const crypto = require("crypto"); // Build error: Node.js module in workflow // [!code highlight]
453
+ return crypto.randomBytes(16);
454
+ }
455
+ ```
456
+
457
+ In fact, Workflow DevKit will throw an error that links to this error page: [Node.js module in workflow](/docs/errors/node-js-module-in-workflow)
458
+
459
+ **3. No closure ambiguity**
460
+
461
+ Steps are transformed into function calls that communicate with the runtime:
462
+
463
+ {/* @skip-typecheck: incomplete code sample */}
464
+ ```typescript lineNumbers
465
+ export async function processOrder(orderId: string) {
466
+ "use workflow";
467
+
468
+ let counter = 0;
469
+
470
+ // This essentially becomes: await enqueueStep("updateCounter", [counter])
471
+ // The step receives counter as a parameter, not a closure
472
+ await updateCounter(counter); // [!code highlight]
473
+
474
+ console.log(counter); // Always 0, consistently // [!code highlight]
475
+ }
476
+ ```
477
+
478
+ Callbacks, however, run inside the workflow sandbox and work as expected:
479
+
480
+ ```typescript lineNumbers
481
+ export async function processOrders(orderIds: string[]) {
482
+ "use workflow";
483
+
484
+ let successCount = 0;
485
+
486
+ // Callbacks run in the workflow context, not skipped on replay
487
+ await Promise.all(orderIds.map(async (orderId) => {
488
+ const order = await fetchOrder(orderId); // Step call
489
+ if (order.status === "completed") {
490
+ successCount++; // Mutation works correctly // [!code highlight]
491
+ }
492
+ }));
493
+
494
+ console.log(successCount); // Consistent across replays
495
+ return { total: orderIds.length, successful: successCount };
496
+ }
497
+ ```
498
+
499
+ The callback runs in the workflow sandbox, so closure reads and mutations behave consistently across replays.
500
+
501
+ **4. Natural syntax**
502
+
503
+ Looks and feels like regular JavaScript:
504
+
505
+ ```typescript lineNumbers
506
+ export async function processOrder(orderId: string) {
507
+ "use workflow";
508
+
509
+ // Standard async/await patterns work naturally // [!code highlight]
510
+ const [order, user] = await Promise.all([ // [!code highlight]
511
+ fetchOrder(orderId), // [!code highlight]
512
+ fetchUser(userId) // [!code highlight]
513
+ ]); // [!code highlight]
514
+
515
+ return { order, user };
516
+ }
517
+ ```
518
+
519
+ **5. Consistent syntax for steps**
520
+
521
+ The `"use step"` directive maintains consistency. While steps run in the full Node.js runtime and *could* work without a directive, they need some way to signal to the workflow runtime that they're steps.
522
+
523
+ We could have used a function wrapper just for steps:
524
+
525
+ {/* @skip-typecheck: incomplete code sample */}
526
+ ```typescript lineNumbers
527
+ // Mixed approach (inconsistent)
528
+ export async function processOrder(orderId: string) {
529
+ "use workflow"; // Directive for workflow // [!code highlight]
530
+
531
+ const order = await step(async () => fetchOrder(orderId));
532
+ return order;
533
+ }
534
+
535
+ const fetchOrder = useStep(() => { // Wrapper for step? // [!code highlight]
536
+ // ...
537
+ })
538
+ ```
539
+
540
+ Mixing syntaxes felt inconsistent.
541
+
542
+ An alternative approach we considered was to treat *all* async function calls as steps by default:
543
+
544
+ ```typescript lineNumbers
545
+ export async function processOrder(orderId: string) {
546
+ "use workflow";
547
+
548
+ // Every async call becomes a step automatically?
549
+ const [order, user] = await Promise.all([ // [!code highlight]
550
+ fetchOrder(orderId), // Step
551
+ fetchUser(userId) // Step
552
+ ]);
553
+
554
+ return { order, user };
555
+ }
556
+ ```
557
+
558
+ This breaks down because many valid async operations inside workflows aren't steps:
559
+
560
+ {/* @skip-typecheck: incomplete code sample */}
561
+ ```typescript lineNumbers
562
+ export async function processOrder(orderId: string) {
563
+ "use workflow";
564
+
565
+ // These are valid async calls that SHOULD NOT be steps:
566
+ const results = await Promise.all([...]); // Language primitive // [!code highlight]
567
+ const winner = await Promise.race([...]); // Language primitive // [!code highlight]
568
+
569
+ // Helper function that formats data
570
+ const formatted = await formatOrderData(order); // Pure JavaScript helper // [!code highlight]
571
+ }
572
+ ```
573
+
574
+ By requiring explicit `"use step"` directives, developers have fine-grained control over what becomes a durable, retryable step versus what runs inline in the workflow sandbox.
575
+
576
+ <Callout>
577
+ To understand how directives are transformed at compile time, see [How the Code Transform Works](/docs/how-it-works/code-transform).
578
+ </Callout>
579
+
580
+ ## What Directives Enable
581
+
582
+ Because `"use workflow"` defines a compile-time semantic boundary, we can provide:
583
+
584
+ <Cards>
585
+ <Card title="Build-Time Validation">
586
+ The compiler catches invalid patterns before deployment: detects disallowed imports, prevents direct side effects, and validates workflow structure.
587
+ </Card>
588
+ <Card title="Static Analysis">
589
+ Analyze workflow code without executing it: generate UML or DAG diagrams automatically, provide observability and visualization, and optimize execution paths.
590
+ </Card>
591
+ <Card title="Durable Execution">
592
+ Workflows can safely suspend and resume: persist execution state between steps, resume from checkpoints after failures or deploys, and scale to zero without losing progress.
593
+ </Card>
594
+ <Card title="Future Optimizations">
595
+ The semantic boundary enables planned improvements: smaller serialized state for faster checkpoints, smarter scheduling based on workflow structure, and more efficient suspension and resumption.
596
+ </Card>
597
+ </Cards>
598
+
599
+ ## Directives as a JavaScript Pattern
600
+
601
+ Directives in JavaScript have always been contracts between the developer and the execution environment. `"use strict"` made this pattern familiar - it's a string literal that changes how code is interpreted.
602
+
603
+ While JavaScript doesn't yet have first-class support for custom directives (like Rust's `#[attribute]` or C++'s `#pragma`), string literal directives are the most pragmatic tool available today.
604
+
605
+ As TC39 members, we at Vercel are actively working with the standards body and broader ecosystem to explore formal specifications for pragma-like syntax or macro annotations that can express execution semantics.
606
+
607
+ ## Closing Thoughts
608
+
609
+ Directives aren't about syntax preference, they're about expressing semantic boundaries. `"use workflow"` tells the compiler, developer, and runtime that this code is deterministic, resumable, and sandboxed.
610
+
611
+ This clarity enables the Workflow Development Kit to provide durable execution with familiar JavaScript patterns, while maintaining the compile-time guarantees necessary for reliable workflow orchestration.