ai-workflows 2.1.3 → 2.4.0

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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +14 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
@@ -0,0 +1,855 @@
1
+ /**
2
+ * Cloudflare Workflows adapter for {@link DurableExecutionAdapter}.
3
+ *
4
+ * Bridges the backend-agnostic port defined in `./durable-execution.ts` to
5
+ * Cloudflare's class-based {@link https://developers.cloudflare.com/workflows/ Workflows runtime}.
6
+ * CF Workflows is the production default per
7
+ * {@link ../../docs/adr/0004-durable-execution-cf-workflows-default.md ADR-0004}:
8
+ * the recently expanded limits (25K steps default-configurable, 50K concurrent
9
+ * instances, 365-day max sleep, 1 GB persisted state) plus zero per-step
10
+ * billing make it the cost-optimal backend for cascade-heavy workloads.
11
+ *
12
+ * ## How the bridge works
13
+ *
14
+ * CF Workflows uses a class-based dispatch model — users author a
15
+ * {@link WorkflowEntrypointLike} subclass with `async run(event, step)`, and
16
+ * the runtime instantiates it for each invocation. Our port's
17
+ * `run(name, fn, input)` is callback-shaped. The adapter resolves the
18
+ * impedance mismatch with two complementary surfaces:
19
+ *
20
+ * 1. **Workflow-function registry.** Callers register named workflow bodies
21
+ * against the adapter via {@link CloudflareWorkflowsDurableExecution.register}
22
+ * (or implicitly the first time {@link CloudflareWorkflowsDurableExecution.run}
23
+ * is called with that name). The registry is a plain {@link Map}
24
+ * maintained on the adapter instance.
25
+ *
26
+ * 2. **`createWorkflowEntrypoint(adapter)`.** Returns a
27
+ * {@link WorkflowEntrypointLike} subclass whose `run(event, step)`
28
+ * reads the workflow name from `event.payload.__wfName`, looks the body
29
+ * up in the registry, and invokes it with a {@link WorkflowContext} that
30
+ * delegates each port primitive to the CF `step` argument. Users export
31
+ * this class from their worker module and wire it through `wrangler.jsonc`:
32
+ *
33
+ * ```jsonc
34
+ * "workflows": [{
35
+ * "name": "cascade",
36
+ * "binding": "WORKFLOW",
37
+ * "class_name": "MyWorkflowEntrypoint"
38
+ * }]
39
+ * ```
40
+ *
41
+ * The adapter's `run()` triggers a workflow by calling `binding.create({
42
+ * params: { __wfName, __wfInput } })` against the supplied
43
+ * {@link WorkflowsBindingLike}. By default it polls
44
+ * `instance.status()` until completion and returns the workflow output;
45
+ * callers that need fire-and-forget can opt out via
46
+ * {@link CloudflareWorkflowsDurableExecutionOptions.waitForCompletion}.
47
+ *
48
+ * ## Step / sleep / waitForEvent translation
49
+ *
50
+ * - `ctx.step(name, fn)` → `step.do(name, fn)`
51
+ * - `ctx.step(name, cfg, fn)` → `step.do(name, cfg, fn)`
52
+ * - `ctx.sleep(duration)` → `step.sleep(autoStepName, duration)`
53
+ * - `ctx.sleepUntil(date)` → `step.sleepUntil(autoStepName, date)`
54
+ * - `ctx.waitForEvent(name, t?)` → `step.waitForEvent(autoStepName, { type: name, timeout: t })`
55
+ *
56
+ * CF requires every primitive to receive a stable name; our port's `sleep` /
57
+ * `sleepUntil` / `waitForEvent` don't. The bridge synthesises a deterministic
58
+ * name from a per-context counter (`__sleep__1`, `__waitForEvent__Order.placed__1`)
59
+ * incremented in body order. Bodies that take the same control-flow path on
60
+ * replay therefore see identical names — the determinism rule applies.
61
+ *
62
+ * Callers who want full control over step naming for sleeps/waits should use
63
+ * {@link DurableExecutionAdapter.step} (or `ctx.step`) wrapping the desired
64
+ * primitive: e.g. `await ctx.step('payment-window', () => sleepUntil(deadline))`.
65
+ *
66
+ * ## Schedules
67
+ *
68
+ * CF Workflows do not support imperative cron registration through the binding;
69
+ * scheduled triggers are declared in `wrangler.jsonc` under `[triggers] crons`
70
+ * and routed to a Worker `scheduled()` handler. The adapter's
71
+ * {@link DurableExecutionAdapter.schedule} therefore registers the workflow
72
+ * body against the adapter and returns a {@link Subscription} whose `id` is
73
+ * the workflow name; the user's worker `scheduled()` handler must call
74
+ * {@link CloudflareWorkflowsDurableExecution.runSchedule} (or the adapter's
75
+ * `run()`) when the cron fires. {@link CloudflareWorkflowsDurableExecution.defineSchedule}
76
+ * is an alias for `schedule()` that emphasises the wrangler-coordinated nature
77
+ * of CF scheduling.
78
+ *
79
+ * ## Rules of Workflows (CF-imposed; universal)
80
+ *
81
+ * 1. **Steps must be idempotent.** CF re-executes step bodies on replay
82
+ * after a hibernation boundary or transient failure. Wrap external
83
+ * side-effects so a duplicate invocation is observably equivalent to a
84
+ * single one.
85
+ *
86
+ * 2. **Step names must be deterministic.** CF uses the step name as the
87
+ * memoization key. Random ids, timestamps, or run-specific values in
88
+ * step names break replay.
89
+ *
90
+ * 3. **State must flow through step returns.** Variables defined in the
91
+ * workflow body but outside a step DO NOT survive hibernation. Read
92
+ * inputs at the top of a step and return only what subsequent steps
93
+ * need.
94
+ *
95
+ * 4. **Workflow bodies must be deterministic.** Two replays of the same
96
+ * input must take the same control-flow path. Push non-determinism
97
+ * (clocks, randomness, network reads) into steps so CF can memoize the
98
+ * result.
99
+ *
100
+ * 5. **Use `step.sleep` / `step.sleepUntil` / `step.waitForEvent`.** Never
101
+ * `setTimeout` or polling — only the runtime knows how to suspend and
102
+ * resume the workflow.
103
+ *
104
+ * ## Limits
105
+ *
106
+ * Declared via {@link CloudflareWorkflowsDurableExecution.limits}:
107
+ *
108
+ * - **Steps per workflow:** 25,000 (default-configurable per Mar 2026
109
+ * change; see ADR-0004).
110
+ * - **Concurrent instances:** 50,000 per account (Apr 2026 change).
111
+ * - **Maximum sleep:** 365 days.
112
+ * - **Per-step / per-event payload:** 1 MiB (CF Workers RPC limit).
113
+ *
114
+ * @see {@link ../../docs/adr/0004-durable-execution-cf-workflows-default.md ADR-0004}
115
+ * @see {@link https://developers.cloudflare.com/workflows/build/workflows-api/ Workflows API}
116
+ * @see {@link https://developers.cloudflare.com/workflows/build/rules-of-workflows/ Rules of Workflows}
117
+ *
118
+ * @example Wiring an adapter
119
+ * ```ts
120
+ * // worker.ts
121
+ * import {
122
+ * createCloudflareWorkflowsDurableExecution,
123
+ * createWorkflowEntrypoint,
124
+ * } from 'ai-workflows/durable-execution'
125
+ *
126
+ * type Env = { WORKFLOW: import('cloudflare:workers').Workflow }
127
+ *
128
+ * const dx = createCloudflareWorkflowsDurableExecution({
129
+ * binding: () => env.WORKFLOW, // resolved per-request
130
+ * })
131
+ *
132
+ * dx.register('cascade', async (ctx) => {
133
+ * const plan = await ctx.step('plan', () => generatePlan(ctx.input))
134
+ * await ctx.sleep('1 minute')
135
+ * return ctx.step('write', () => writeAll(plan))
136
+ * })
137
+ *
138
+ * // The class wrangler binds; CF instantiates it on each run.
139
+ * export const MyWorkflow = createWorkflowEntrypoint(dx)
140
+ *
141
+ * export default {
142
+ * async fetch(req: Request, env: Env) {
143
+ * // Trigger from anywhere — adapter.run() goes through the binding.
144
+ * const result = await dx.run('cascade', undefined as never, { customerId: 'c-1' })
145
+ * return Response.json(result)
146
+ * },
147
+ * }
148
+ * ```
149
+ *
150
+ * @packageDocumentation
151
+ */
152
+
153
+ import {
154
+ DurableStepError,
155
+ WaitForEventTimeoutError,
156
+ type DurableExecutionAdapter,
157
+ type DurableExecutionKind,
158
+ type StepConfig,
159
+ type Subscription,
160
+ type WorkflowContext,
161
+ type WorkflowFn,
162
+ } from './durable-execution.js'
163
+
164
+ // =============================================================================
165
+ // Cloudflare Workflows shape (declared structurally for testability)
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Minimal `WorkflowStep` shape — only the methods this adapter uses.
170
+ *
171
+ * Defined structurally so the adapter compiles in Node test environments and
172
+ * accepts a pure-JS fake without depending on `cloudflare:workers` at runtime.
173
+ * In production, callers pass the real `step` argument provided by CF to
174
+ * their {@link WorkflowEntrypointLike.run} method.
175
+ */
176
+ export interface WorkflowStepLike {
177
+ do<T>(name: string, callback: () => Promise<T>): Promise<T>
178
+ do<T>(name: string, config: WorkflowStepConfigLike, callback: () => Promise<T>): Promise<T>
179
+ sleep(name: string, duration: string | number): Promise<void>
180
+ sleepUntil(name: string, timestamp: Date | number): Promise<void>
181
+ waitForEvent<T = unknown>(
182
+ name: string,
183
+ options: { type: string; timeout?: string | number }
184
+ ): Promise<{ payload: T; type: string; timestamp: Date } | T>
185
+ }
186
+
187
+ /**
188
+ * Minimal `WorkflowStepConfig` shape passed to `step.do(name, config, fn)`.
189
+ * Mirrors CF's `WorkflowStepConfig`. Compatible with our port's
190
+ * {@link StepConfig}.
191
+ */
192
+ export interface WorkflowStepConfigLike {
193
+ retries?: {
194
+ limit: number
195
+ delay: string | number
196
+ backoff?: 'constant' | 'linear' | 'exponential'
197
+ }
198
+ timeout?: string | number
199
+ }
200
+
201
+ /**
202
+ * Minimal `WorkflowEvent<T>` shape — only the fields this adapter touches.
203
+ * `payload` carries the `{ __wfName, __wfInput }` envelope produced by
204
+ * {@link CloudflareWorkflowsDurableExecution.run}.
205
+ */
206
+ export interface WorkflowEventLike<T = unknown> {
207
+ readonly payload: Readonly<T>
208
+ readonly timestamp?: Date
209
+ readonly instanceId?: string
210
+ }
211
+
212
+ /**
213
+ * Minimal `WorkflowInstance` shape — only the methods this adapter uses.
214
+ *
215
+ * Declared structurally to match CF's `WorkflowInstance` while remaining
216
+ * testable without `cloudflare:workers`.
217
+ */
218
+ export interface WorkflowInstanceLike {
219
+ readonly id: string
220
+ status(): Promise<{
221
+ status:
222
+ | 'queued'
223
+ | 'running'
224
+ | 'paused'
225
+ | 'errored'
226
+ | 'terminated'
227
+ | 'complete'
228
+ | 'waiting'
229
+ | 'waitingForPause'
230
+ | 'unknown'
231
+ error?: { name: string; message: string }
232
+ output?: unknown
233
+ }>
234
+ sendEvent?(args: { type: string; payload: unknown }): Promise<void>
235
+ }
236
+
237
+ /**
238
+ * Minimal `Workflow` binding shape — only the methods this adapter uses.
239
+ *
240
+ * Mirrors `Workflow<PARAMS>` from `cloudflare:workers`. The adapter accepts
241
+ * either the binding directly or a thunk that returns it (for environments
242
+ * where the binding is only resolvable per-request, e.g. inside `fetch`).
243
+ */
244
+ export interface WorkflowsBindingLike<PARAMS = unknown> {
245
+ create(options?: { id?: string; params?: PARAMS }): Promise<WorkflowInstanceLike>
246
+ get(id: string): Promise<WorkflowInstanceLike>
247
+ }
248
+
249
+ /**
250
+ * Base class shape `createWorkflowEntrypoint` extends. Aliased here so users
251
+ * (and tests) can refer to the structural type without importing
252
+ * `cloudflare:workers`. Real callers receive an actual `WorkflowEntrypoint`
253
+ * subclass; tests can stub it.
254
+ */
255
+ export interface WorkflowEntrypointLike<Env = unknown, T = unknown> {
256
+ run(event: Readonly<WorkflowEventLike<T>>, step: WorkflowStepLike): Promise<unknown>
257
+ }
258
+
259
+ // =============================================================================
260
+ // Adapter options
261
+ // =============================================================================
262
+
263
+ /**
264
+ * Options for {@link createCloudflareWorkflowsDurableExecution}.
265
+ */
266
+ export interface CloudflareWorkflowsDurableExecutionOptions {
267
+ /**
268
+ * The CF Workflows binding from a Workers environment, or a thunk that
269
+ * resolves the binding per-call. Use the thunk form when the binding is
270
+ * only available inside a request handler (e.g. `() => env.WORKFLOW`).
271
+ */
272
+ binding: WorkflowsBindingLike | (() => WorkflowsBindingLike)
273
+
274
+ /**
275
+ * When `true` (default), {@link DurableExecutionAdapter.run} polls the
276
+ * created instance's status until it reaches `complete` or `errored` and
277
+ * returns the workflow output (or throws on `errored`/`terminated`).
278
+ *
279
+ * When `false`, `run()` returns the {@link WorkflowInstanceLike} cast to
280
+ * `unknown` so the caller can manage the lifecycle themselves. Useful for
281
+ * fire-and-forget triggers or when polling is undesirable.
282
+ *
283
+ * Default: `true`.
284
+ */
285
+ waitForCompletion?: boolean
286
+
287
+ /**
288
+ * Polling interval (ms) when `waitForCompletion` is `true`. Default: 250.
289
+ */
290
+ pollIntervalMs?: number
291
+
292
+ /**
293
+ * Maximum total wait time (ms) when `waitForCompletion` is `true`. After
294
+ * this elapses the adapter throws a {@link DurableStepError} with
295
+ * `retryable: false`. Default: 24 hours (86_400_000 ms). Set higher for
296
+ * long-sleeping workflows or use `waitForCompletion: false`.
297
+ */
298
+ pollTimeoutMs?: number
299
+
300
+ /**
301
+ * Override `setTimeout` for tests. Defaults to the global `setTimeout`.
302
+ * Used by the polling loop and the (no-op) schedule registry.
303
+ */
304
+ delay?: (ms: number) => Promise<void>
305
+ }
306
+
307
+ /**
308
+ * Limits the adapter knows about (per ADR-0004 / current CF docs).
309
+ */
310
+ export interface CloudflareWorkflowsLimits {
311
+ /** Maximum steps per workflow (default-configurable as of Mar 2026). */
312
+ readonly maxSteps: 25_000
313
+ /** Maximum concurrent workflow instances per account. */
314
+ readonly maxConcurrentInstances: 50_000
315
+ /** Maximum single sleep / sleepUntil duration. */
316
+ readonly maxSleepDays: 365
317
+ /** Per-step / per-event payload size (CF Workers RPC limit). */
318
+ readonly maxPayloadBytes: 1_048_576
319
+ }
320
+
321
+ const CLOUDFLARE_WORKFLOWS_LIMITS: CloudflareWorkflowsLimits = {
322
+ maxSteps: 25_000,
323
+ maxConcurrentInstances: 50_000,
324
+ maxSleepDays: 365,
325
+ maxPayloadBytes: 1_048_576,
326
+ }
327
+
328
+ // =============================================================================
329
+ // Internal: payload envelope
330
+ // =============================================================================
331
+
332
+ /**
333
+ * Internal envelope passed as the `params` to `binding.create()`. The
334
+ * generated {@link WorkflowEntrypointLike} reads these fields from
335
+ * `event.payload` to dispatch into the registered workflow body.
336
+ *
337
+ * Names are double-underscored to avoid collision with user-supplied input
338
+ * keys when input is itself an object literal — though we always wrap input
339
+ * inside `__wfInput` so this is belt-and-suspenders.
340
+ */
341
+ export interface WorkflowEnvelope<TInput = unknown> {
342
+ readonly __wfName: string
343
+ readonly __wfInput: TInput
344
+ }
345
+
346
+ function isEnvelope(payload: unknown): payload is WorkflowEnvelope {
347
+ return (
348
+ typeof payload === 'object' &&
349
+ payload !== null &&
350
+ '__wfName' in payload &&
351
+ typeof (payload as { __wfName?: unknown }).__wfName === 'string' &&
352
+ '__wfInput' in payload
353
+ )
354
+ }
355
+
356
+ // =============================================================================
357
+ // Adapter
358
+ // =============================================================================
359
+
360
+ /**
361
+ * The adapter returned by {@link createCloudflareWorkflowsDurableExecution}.
362
+ *
363
+ * Extends {@link DurableExecutionAdapter} with:
364
+ *
365
+ * - {@link CloudflareWorkflowsDurableExecution.register} — explicitly
366
+ * register a workflow body before triggering it.
367
+ * - {@link CloudflareWorkflowsDurableExecution.defineSchedule} — alias for
368
+ * `schedule()` whose name emphasises the wrangler-coordinated nature.
369
+ * - {@link CloudflareWorkflowsDurableExecution.runSchedule} — invoke a
370
+ * scheduled workflow from a Worker `scheduled()` handler when its cron
371
+ * fires.
372
+ * - {@link CloudflareWorkflowsDurableExecution.entrypointHandler} — bound
373
+ * handler used by the {@link WorkflowEntrypointLike} class to dispatch
374
+ * incoming events back into the registered body.
375
+ * - {@link CloudflareWorkflowsDurableExecution.limits} — declared CF
376
+ * Workflows limits (consumers can introspect to bound their own steps).
377
+ */
378
+ export interface CloudflareWorkflowsDurableExecution extends DurableExecutionAdapter {
379
+ readonly kind: 'cloudflare'
380
+ readonly limits: CloudflareWorkflowsLimits
381
+
382
+ /**
383
+ * Register a workflow body under `name`. Subsequent `run(name, ...)` and
384
+ * scheduled invocations dispatch to it. Calling `register` with an
385
+ * existing name replaces the previous body.
386
+ */
387
+ register<TResult = unknown, TInput = unknown>(name: string, fn: WorkflowFn<TResult, TInput>): void
388
+
389
+ /**
390
+ * Alias for {@link DurableExecutionAdapter.schedule}. CF Workflows have no
391
+ * imperative cron API; the user's wrangler config defines the schedule
392
+ * (`[triggers] crons = [...]`) and routes to a `scheduled()` handler that
393
+ * calls {@link runSchedule} when the cron fires.
394
+ *
395
+ * Returns a {@link Subscription} whose `id` equals `name`.
396
+ */
397
+ defineSchedule<TResult = unknown>(
398
+ name: string,
399
+ cron: string,
400
+ fn: WorkflowFn<TResult, undefined>
401
+ ): Subscription
402
+
403
+ /**
404
+ * Invoke a previously registered scheduled workflow. Intended to be called
405
+ * from a Worker `scheduled()` handler when the cron fires. Returns the
406
+ * triggered instance handle (or its output if `waitForCompletion`).
407
+ */
408
+ runSchedule(name: string): Promise<unknown>
409
+
410
+ /**
411
+ * The dispatch handler used by the generated
412
+ * {@link WorkflowEntrypointLike} class. Exposed so callers can build their
413
+ * own entrypoint subclass (e.g. to layer custom env-handling) and forward
414
+ * `(event, step)` here.
415
+ */
416
+ entrypointHandler(event: WorkflowEventLike, step: WorkflowStepLike): Promise<unknown>
417
+ }
418
+
419
+ /**
420
+ * Construct a Cloudflare Workflows {@link DurableExecutionAdapter}.
421
+ *
422
+ * The adapter satisfies the full port contract by translating each call into
423
+ * its CF equivalent (see module docs). Step bodies execute inside CF's
424
+ * runtime and inherit CF's idempotency, replay, and hibernation semantics.
425
+ *
426
+ * **The adapter does not run workflow bodies in-process.** Calling `run()`
427
+ * triggers a CF Workflow via the supplied binding; the body runs on CF's
428
+ * infrastructure and the adapter polls (or returns the instance handle).
429
+ * For tests that need to exercise the body directly without a CF runtime,
430
+ * use {@link createInProcessDurableExecution} or
431
+ * {@link createInMemoryDurableExecution} instead.
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * import { createCloudflareWorkflowsDurableExecution, createWorkflowEntrypoint } from 'ai-workflows/durable-execution'
436
+ *
437
+ * type Env = { WORKFLOW: Workflow }
438
+ *
439
+ * const dx = createCloudflareWorkflowsDurableExecution({ binding: () => env.WORKFLOW })
440
+ * dx.register('hello', async (ctx) => `hi, ${ctx.input}`)
441
+ *
442
+ * export const HelloWorkflow = createWorkflowEntrypoint(dx)
443
+ * ```
444
+ */
445
+ export function createCloudflareWorkflowsDurableExecution(
446
+ options: CloudflareWorkflowsDurableExecutionOptions
447
+ ): CloudflareWorkflowsDurableExecution {
448
+ const waitForCompletion = options.waitForCompletion ?? true
449
+ const pollIntervalMs = options.pollIntervalMs ?? 250
450
+ const pollTimeoutMs = options.pollTimeoutMs ?? 24 * 60 * 60 * 1000
451
+ const delay =
452
+ options.delay ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
453
+
454
+ const registry = new Map<string, WorkflowFn>()
455
+ const schedules = new Map<string, { cron: string; fn: WorkflowFn<unknown, undefined> }>()
456
+
457
+ function resolveBinding(): WorkflowsBindingLike {
458
+ const b = options.binding
459
+ return typeof b === 'function' ? b() : b
460
+ }
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // Workflow context bridge — invoked by the entrypoint handler with the CF
464
+ // `step` argument. Each port primitive translates to the matching CF call.
465
+ // ---------------------------------------------------------------------------
466
+
467
+ function buildContext<TInput>(
468
+ name: string,
469
+ instanceId: string,
470
+ input: TInput,
471
+ step: WorkflowStepLike
472
+ ): WorkflowContext<TInput> {
473
+ // Auto-naming counter for sleep/waitForEvent (CF requires every primitive
474
+ // to receive a stable, deterministic name). The body's control flow is
475
+ // deterministic, so a strictly increasing counter yields stable names
476
+ // across replays.
477
+ let sleepCounter = 0
478
+ let sleepUntilCounter = 0
479
+ const waitCounters = new Map<string, number>()
480
+
481
+ const ctx: WorkflowContext<TInput> = {
482
+ input,
483
+ instanceId,
484
+ name,
485
+ step: ((
486
+ stepName: string,
487
+ configOrFn: StepConfig | (() => Promise<unknown>),
488
+ maybeFn?: () => Promise<unknown>
489
+ ) => {
490
+ if (typeof configOrFn === 'function') {
491
+ return step.do(stepName, configOrFn) as Promise<unknown>
492
+ }
493
+ return step.do(stepName, configOrFn as WorkflowStepConfigLike, maybeFn!) as Promise<unknown>
494
+ }) as WorkflowContext<TInput>['step'],
495
+ async sleep(duration) {
496
+ const stepName = `__sleep__${++sleepCounter}`
497
+ await step.sleep(stepName, duration as string | number)
498
+ },
499
+ async sleepUntil(date) {
500
+ const stepName = `__sleepUntil__${++sleepUntilCounter}`
501
+ await step.sleepUntil(stepName, date)
502
+ },
503
+ async waitForEvent<T = unknown>(eventName: string, timeout?: number | string): Promise<T> {
504
+ const seq = (waitCounters.get(eventName) ?? 0) + 1
505
+ waitCounters.set(eventName, seq)
506
+ const stepName = `__waitForEvent__${eventName}__${seq}`
507
+ try {
508
+ const opts: { type: string; timeout?: string | number } = { type: eventName }
509
+ if (timeout !== undefined) opts.timeout = timeout
510
+ const result = await step.waitForEvent<T>(stepName, opts)
511
+ // CF returns { payload, type, timestamp } — unwrap to the user-shaped
512
+ // value when the envelope is present; otherwise pass through.
513
+ if (
514
+ result !== null &&
515
+ typeof result === 'object' &&
516
+ 'payload' in (result as Record<string, unknown>)
517
+ ) {
518
+ return (result as { payload: T }).payload
519
+ }
520
+ return result as T
521
+ } catch (err) {
522
+ // CF surfaces wait timeouts as thrown errors; normalise to the
523
+ // port's WaitForEventTimeoutError when the message indicates a
524
+ // timeout. Otherwise rethrow.
525
+ const msg = err instanceof Error ? err.message : String(err)
526
+ if (/timeout|timed out/i.test(msg) && timeout !== undefined) {
527
+ throw new WaitForEventTimeoutError(eventName, timeout)
528
+ }
529
+ throw err
530
+ }
531
+ },
532
+ }
533
+ return ctx
534
+ }
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // Entrypoint handler — invoked by the generated WorkflowEntrypoint class.
538
+ // ---------------------------------------------------------------------------
539
+
540
+ async function entrypointHandler(
541
+ event: WorkflowEventLike,
542
+ step: WorkflowStepLike
543
+ ): Promise<unknown> {
544
+ const payload = event.payload
545
+ if (!isEnvelope(payload)) {
546
+ throw new Error(
547
+ 'Cloudflare Workflows adapter: workflow event payload missing __wfName/__wfInput envelope. ' +
548
+ 'Workflows triggered through this adapter must be created via adapter.run() or adapter.runSchedule().'
549
+ )
550
+ }
551
+ const fn = registry.get(payload.__wfName)
552
+ if (!fn) {
553
+ throw new Error(
554
+ `Cloudflare Workflows adapter: no workflow registered for name "${payload.__wfName}". ` +
555
+ 'Call adapter.register(name, fn) before triggering it.'
556
+ )
557
+ }
558
+ const instanceId = event.instanceId ?? `cf-${Date.now()}`
559
+ const ctx = buildContext(payload.__wfName, instanceId, payload.__wfInput, step)
560
+ return fn(ctx)
561
+ }
562
+
563
+ // ---------------------------------------------------------------------------
564
+ // Polling — wait for an instance to terminate.
565
+ // ---------------------------------------------------------------------------
566
+
567
+ async function pollUntilDone(instance: WorkflowInstanceLike): Promise<unknown> {
568
+ const start = Date.now()
569
+ // First check is unconditional so quick-completing workflows return
570
+ // without an extra delay tick.
571
+ let last = await instance.status()
572
+ while (true) {
573
+ if (last.status === 'complete') return last.output
574
+ if (last.status === 'errored' || last.status === 'terminated') {
575
+ const message =
576
+ last.error?.message ?? `Workflow instance ${instance.id} ended with status ${last.status}`
577
+ throw new DurableStepError(message, {
578
+ stepName: `<workflow:${instance.id}>`,
579
+ attempts: 1,
580
+ retryable: false,
581
+ cause: last.error ?? new Error(message),
582
+ })
583
+ }
584
+ if (Date.now() - start > pollTimeoutMs) {
585
+ throw new DurableStepError(
586
+ `Workflow instance ${instance.id} did not complete within ${pollTimeoutMs}ms`,
587
+ {
588
+ stepName: `<workflow:${instance.id}>`,
589
+ attempts: 1,
590
+ retryable: false,
591
+ cause: new Error('poll timeout'),
592
+ }
593
+ )
594
+ }
595
+ await delay(pollIntervalMs)
596
+ last = await instance.status()
597
+ }
598
+ }
599
+
600
+ // ---------------------------------------------------------------------------
601
+ // Public surface
602
+ // ---------------------------------------------------------------------------
603
+
604
+ function register<TResult = unknown, TInput = unknown>(
605
+ name: string,
606
+ fn: WorkflowFn<TResult, TInput>
607
+ ): void {
608
+ registry.set(name, fn as WorkflowFn)
609
+ }
610
+
611
+ async function run<TResult = unknown, TInput = unknown>(
612
+ name: string,
613
+ fn: WorkflowFn<TResult, TInput>,
614
+ input: TInput
615
+ ): Promise<TResult> {
616
+ // Implicitly register if the caller supplied a body. Allows the
617
+ // canonical port shape `dx.run(name, fn, input)` without a separate
618
+ // register() step. If the same name is later run() with no body, the
619
+ // most recent registration wins — matching the in-process adapter.
620
+ if (typeof fn === 'function') {
621
+ registry.set(name, fn as WorkflowFn)
622
+ }
623
+ const binding = resolveBinding()
624
+ const envelope: WorkflowEnvelope<TInput> = { __wfName: name, __wfInput: input }
625
+ const instance = await binding.create({ params: envelope })
626
+ if (!waitForCompletion) {
627
+ // Caller manages the instance; return it cast through unknown.
628
+ return instance as unknown as TResult
629
+ }
630
+ return (await pollUntilDone(instance)) as TResult
631
+ }
632
+
633
+ async function step<T>(
634
+ name: string,
635
+ configOrFn: StepConfig | (() => Promise<T>),
636
+ maybeFn?: () => Promise<T>
637
+ ): Promise<T> {
638
+ // Outside a workflow body CF Workflows have no step concept — the binding
639
+ // can't run a single step in isolation. We mirror the in-memory stub's
640
+ // behaviour for callers using `dx.step()` outside a `run`: execute the
641
+ // function directly without memoization. Inside a body, the
642
+ // {@link WorkflowContext.step} delegate is used (which translates to
643
+ // `step.do`).
644
+ void name
645
+ const fn = typeof configOrFn === 'function' ? configOrFn : maybeFn!
646
+ return fn()
647
+ }
648
+
649
+ async function sleep(duration: string | number): Promise<void> {
650
+ // Outside a body, sleep is just a delay — there's no CF step context to
651
+ // suspend. This matches the in-memory stub.
652
+ const ms = typeof duration === 'number' ? duration : parseDurationLoose(duration)
653
+ if (ms > 0) await delay(ms)
654
+ }
655
+
656
+ async function sleepUntil(date: Date): Promise<void> {
657
+ const ms = date.getTime() - Date.now()
658
+ if (ms > 0) await delay(ms)
659
+ }
660
+
661
+ function waitForEvent<T = unknown>(name: string, timeout?: number | string): Promise<T> {
662
+ // Outside a body there is no CF step.waitForEvent surface. Reject so
663
+ // callers don't accidentally rely on hibernation-style waits without a
664
+ // running workflow.
665
+ return Promise.reject(
666
+ new Error(
667
+ `Cloudflare Workflows adapter: waitForEvent("${name}"${
668
+ timeout !== undefined ? `, ${String(timeout)}` : ''
669
+ }) ` +
670
+ 'is only supported inside a workflow body (ctx.waitForEvent). ' +
671
+ 'Trigger the workflow via adapter.run() and call waitForEvent on its ctx.'
672
+ )
673
+ )
674
+ }
675
+
676
+ function schedule<TResult = unknown>(
677
+ name: string,
678
+ cron: string,
679
+ fn: WorkflowFn<TResult, undefined>
680
+ ): Subscription {
681
+ schedules.set(name, { cron, fn: fn as WorkflowFn<unknown, undefined> })
682
+ registry.set(name, fn as WorkflowFn)
683
+ return {
684
+ id: name,
685
+ unsubscribe(): void {
686
+ schedules.delete(name)
687
+ // Leave the body in registry so already-fired runs still resolve.
688
+ },
689
+ }
690
+ }
691
+
692
+ function defineSchedule<TResult = unknown>(
693
+ name: string,
694
+ cron: string,
695
+ fn: WorkflowFn<TResult, undefined>
696
+ ): Subscription {
697
+ return schedule(name, cron, fn)
698
+ }
699
+
700
+ async function runSchedule(name: string): Promise<unknown> {
701
+ const entry = schedules.get(name)
702
+ if (!entry) {
703
+ throw new Error(
704
+ `Cloudflare Workflows adapter: no schedule registered for "${name}". ` +
705
+ 'Call adapter.defineSchedule(name, cron, fn) before invoking runSchedule.'
706
+ )
707
+ }
708
+ return run(name, entry.fn, undefined)
709
+ }
710
+
711
+ const adapter: CloudflareWorkflowsDurableExecution = {
712
+ kind: 'cloudflare' as DurableExecutionKind & 'cloudflare',
713
+ limits: CLOUDFLARE_WORKFLOWS_LIMITS,
714
+ register,
715
+ run: run as DurableExecutionAdapter['run'],
716
+ step: step as DurableExecutionAdapter['step'],
717
+ sleep,
718
+ sleepUntil,
719
+ waitForEvent,
720
+ schedule,
721
+ defineSchedule,
722
+ runSchedule,
723
+ entrypointHandler,
724
+ }
725
+
726
+ return adapter
727
+ }
728
+
729
+ // =============================================================================
730
+ // WorkflowEntrypoint factory
731
+ // =============================================================================
732
+
733
+ /**
734
+ * Constructor-shape returned by {@link createWorkflowEntrypoint} when called
735
+ * without the optional `Base` class. Callable with `new` like a normal
736
+ * `WorkflowEntrypoint` subclass — CF instantiates it once per workflow run.
737
+ *
738
+ * The first constructor argument is `ctx` (an `ExecutionContext`) and the
739
+ * second is `env`; declared `unknown` here so this module compiles without
740
+ * `cloudflare:workers` types in the build environment.
741
+ */
742
+ export type WorkflowEntrypointConstructor = new (ctx: unknown, env: unknown) => {
743
+ run(event: WorkflowEventLike, step: WorkflowStepLike): Promise<unknown>
744
+ }
745
+
746
+ /**
747
+ * Generate a {@link WorkflowEntrypointLike} subclass bound to `adapter`.
748
+ *
749
+ * Users export the returned class from their worker module and reference it
750
+ * in `wrangler.jsonc` under `workflows[].class_name`. CF instantiates the
751
+ * class for each workflow run, calls its `run(event, step)`, and the adapter
752
+ * dispatches into the registered body.
753
+ *
754
+ * **Why a factory instead of a fixed export?** CF binds workflows by class
755
+ * name at deploy time — each binding needs its own class identity. The
756
+ * factory pattern lets users have multiple adapters/classes in one module
757
+ * (e.g. one per binding) without colliding.
758
+ *
759
+ * If `Base` is omitted, this module declares an internal abstract class with
760
+ * the signature CF expects. In production, callers SHOULD pass the real
761
+ * `WorkflowEntrypoint` from `cloudflare:workers` so CF's runtime magic (env
762
+ * injection, `[Rpc.__WORKFLOW_ENTRYPOINT_BRAND]`, etc.) is preserved:
763
+ *
764
+ * ```ts
765
+ * import { WorkflowEntrypoint } from 'cloudflare:workers'
766
+ * import { createCloudflareWorkflowsDurableExecution, createWorkflowEntrypoint } from 'ai-workflows/durable-execution'
767
+ *
768
+ * const dx = createCloudflareWorkflowsDurableExecution({ binding: () => env.WORKFLOW })
769
+ * export const MyWorkflow = createWorkflowEntrypoint(dx, WorkflowEntrypoint)
770
+ * ```
771
+ *
772
+ * In tests / Node environments where `cloudflare:workers` isn't available,
773
+ * call without `Base` and the adapter's dispatch logic is exercised against
774
+ * the structural fake.
775
+ */
776
+ export function createWorkflowEntrypoint<Env = unknown, T = unknown>(
777
+ adapter: CloudflareWorkflowsDurableExecution,
778
+ Base?: WorkflowEntrypointConstructor
779
+ ): WorkflowEntrypointConstructor {
780
+ if (Base) {
781
+ return class extends Base {
782
+ override async run(event: WorkflowEventLike<T>, step: WorkflowStepLike): Promise<unknown> {
783
+ return adapter.entrypointHandler(event, step)
784
+ }
785
+ } as unknown as WorkflowEntrypointConstructor
786
+ }
787
+ // Pure-JS shim used when callers don't (or can't) supply the real CF base
788
+ // class. Sufficient for unit tests of the dispatch path; production callers
789
+ // should always pass `WorkflowEntrypoint` from `cloudflare:workers`.
790
+ return class WorkflowEntrypointShim {
791
+ constructor(_ctx: unknown, _env: unknown) {
792
+ // Match CF's two-arg constructor shape; we don't need the values.
793
+ void _ctx
794
+ void _env
795
+ }
796
+ async run(event: WorkflowEventLike<T>, step: WorkflowStepLike): Promise<unknown> {
797
+ return adapter.entrypointHandler(event, step)
798
+ }
799
+ } as unknown as WorkflowEntrypointConstructor
800
+ }
801
+
802
+ // =============================================================================
803
+ // Internal: minimal duration parser (top-level sleep outside a body)
804
+ // =============================================================================
805
+
806
+ /**
807
+ * Tolerant duration parser used only by the top-level `sleep()` outside a
808
+ * body. Inside a body, durations are passed verbatim to CF's `step.sleep`,
809
+ * which has its own grammar. We do not constrain string forms here as
810
+ * tightly as the in-memory stub since CF accepts a richer grammar
811
+ * (`'10 seconds'`, `'1 day'`, etc.).
812
+ */
813
+ function parseDurationLoose(input: string): number {
814
+ const trimmed = input.trim().toLowerCase()
815
+ const match = trimmed.match(
816
+ /^(\d+(?:\.\d+)?)\s*(ms|millisecond|milliseconds|s|sec|second|seconds|m|min|minute|minutes|h|hr|hour|hours|d|day|days|w|week|weeks)$/
817
+ )
818
+ if (!match) {
819
+ throw new Error(`Cloudflare Workflows adapter: unrecognised duration "${input}"`)
820
+ }
821
+ const value = parseFloat(match[1]!)
822
+ const unit = match[2]!
823
+ switch (unit) {
824
+ case 'ms':
825
+ case 'millisecond':
826
+ case 'milliseconds':
827
+ return value
828
+ case 's':
829
+ case 'sec':
830
+ case 'second':
831
+ case 'seconds':
832
+ return value * 1000
833
+ case 'm':
834
+ case 'min':
835
+ case 'minute':
836
+ case 'minutes':
837
+ return value * 60_000
838
+ case 'h':
839
+ case 'hr':
840
+ case 'hour':
841
+ case 'hours':
842
+ return value * 3_600_000
843
+ case 'd':
844
+ case 'day':
845
+ case 'days':
846
+ return value * 86_400_000
847
+ case 'w':
848
+ case 'week':
849
+ case 'weeks':
850
+ return value * 604_800_000
851
+ /* istanbul ignore next */
852
+ default:
853
+ throw new Error(`Cloudflare Workflows adapter: unrecognised duration unit "${unit}"`)
854
+ }
855
+ }