@voltro/workflow 0.1.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.
@@ -0,0 +1,865 @@
1
+ import { Cause } from 'effect';
2
+ import { Context } from 'effect';
3
+ import { DurableClock as durableClock } from '@effect/workflow';
4
+ import { DurableQueue as durableQueueModule } from '@effect/workflow';
5
+ import { DurableRateLimiter as durableRateLimiterModule } from '@effect/workflow';
6
+ import { Effect } from 'effect';
7
+ import { FiberRef } from 'effect';
8
+ import { Layer } from 'effect';
9
+ import { ParseResult } from 'effect';
10
+ import { QueryDescriptor } from '@voltro/database';
11
+ import { Row } from '@voltro/database';
12
+ import { Schema } from 'effect';
13
+ import { SchemaTable } from '@voltro/database';
14
+ import { Activity as stepModule } from '@effect/workflow';
15
+ import { Subject } from '@voltro/protocol';
16
+ import { TableLike } from '@voltro/database';
17
+ import { WorkflowEngine } from '@effect/workflow';
18
+ import { Workflow as workflowModule } from '@effect/workflow';
19
+ import { WorkflowParentClosePolicy } from '@voltro/protocol';
20
+
21
+ /**
22
+ * Wait inside a workflow body for an externally-injected signal.
23
+ *
24
+ * Usage:
25
+ * ```ts
26
+ * const buildExecute = (ctx: AppContext) =>
27
+ * (input: { ... }) =>
28
+ * Effect.gen(function* () {
29
+ * // ... earlier steps ...
30
+ * const decision = yield* awaitSignal(ctx, {
31
+ * name: 'approval',
32
+ * schema: Schema.Struct({ approved: Schema.Boolean }),
33
+ * })
34
+ * // `decision: { approved: boolean }` — typed by the schema.
35
+ * if (!decision.approved) return { status: 'rejected' as const }
36
+ * // ... continue ...
37
+ * })
38
+ * ```
39
+ *
40
+ * From the dashboard, click "Send signal" on the run row and pick
41
+ * `approval` with a JSON body matching the schema.
42
+ */
43
+ export declare const awaitSignal: <A>(ctx: AwaitSignalCtx, options: AwaitSignalOptions<A>) => Effect.Effect<A, Cause.UnknownException | ParseResult.ParseError>;
44
+
45
+ /** The slice of `ctx.store` awaitSignal needs — just `query`, typed against
46
+ * the real `QueryDescriptor` so a full `AppContext` store (`DataStore`) is
47
+ * assignable. This is what makes the documented `awaitSignal(ctx, …)` call
48
+ * inside a workflow executor typecheck. (`@voltro/workflow` already depends on
49
+ * `@voltro/database` — see the `and`/`eq` import above — so this adds no new
50
+ * package edge.) */
51
+ export declare interface AwaitSignalCtx {
52
+ readonly store: {
53
+ readonly query: (descriptor: QueryDescriptor) => Promise<ReadonlyArray<unknown>>;
54
+ };
55
+ }
56
+
57
+ export declare interface AwaitSignalOptions<A> {
58
+ /** Caller-chosen signal name. Mirror this in the dashboard's "Send
59
+ * signal" modal — the inspect bus matches on this exact string. */
60
+ readonly name: string;
61
+ /** Schema the incoming payload is parsed against. Parse failures
62
+ * fail the activity with a typed error (the schema's ParseError);
63
+ * fix the sender's payload shape and re-send. */
64
+ readonly schema: Schema.Schema<A, any>;
65
+ /** Initial poll interval in milliseconds. Default 200 ms — gives
66
+ * sub-second wake for fresh signals. Subsequent polls back off
67
+ * exponentially up to `maxPollIntervalMs`. */
68
+ readonly pollIntervalMs?: number;
69
+ /** Upper bound on the exponential backoff. Default 5000 ms. The
70
+ * poll loop never sleeps longer than this between checks. */
71
+ readonly maxPollIntervalMs?: number;
72
+ /** Max time to wait before failing the activity. Default 24 h.
73
+ * Pass `Number.POSITIVE_INFINITY` for an unbounded wait. */
74
+ readonly timeoutMs?: number;
75
+ }
76
+
77
+ /**
78
+ * Wait inside a workflow body for an externally-injected signal, SUSPENDING
79
+ * the run (freeing its worker) until the signal arrives or the timeout fires.
80
+ *
81
+ * Drop-in shape-compatible with {@link awaitSignal}; prefer this variant for
82
+ * waits that may last minutes/hours/days (human-in-the-loop, AI-Flows) so a
83
+ * parked run does not pin a fiber.
84
+ *
85
+ * ```ts
86
+ * const decision = yield* awaitSignalSuspending(ctx, {
87
+ * name: 'approval',
88
+ * schema: Schema.Struct({ approved: Schema.Boolean }),
89
+ * })
90
+ * // `decision: { approved: boolean }` — the run was Suspended until an
91
+ * // external `ctx.workflows.signal(target, 'approval', { approved })` resumed it.
92
+ * ```
93
+ *
94
+ * `ctx` is accepted for signature parity with `awaitSignal` (so the two are
95
+ * interchangeable in a body); the suspending variant reads the durable
96
+ * deferred, not `ctx.store`.
97
+ */
98
+ export declare const awaitSignalSuspending: <A>(ctx: AwaitSignalCtx, options: AwaitSignalSuspendingOptions<A>) => Effect.Effect<A, ParseResult.ParseError>;
99
+
100
+ export declare interface AwaitSignalSuspendingOptions<A> {
101
+ /** Caller-chosen signal name. The external completer targets this exact
102
+ * string — mirror it in the dashboard's "Send signal" modal. */
103
+ readonly name: string;
104
+ /** Schema the incoming payload is decoded against. A decode failure fails
105
+ * the workflow with the schema's `ParseError`. */
106
+ readonly schema: Schema.Schema<A, any>;
107
+ /** Max time to wait before failing the workflow. Default 24 h. The timeout
108
+ * is itself durable (a raced `DurableClock`), so a long wait frees the
109
+ * runner too. Pass `Number.POSITIVE_INFINITY` for an unbounded wait. */
110
+ readonly timeoutMs?: number;
111
+ }
112
+
113
+ export declare const awaitUpdate: <A, B = A>(ctx: AwaitUpdateCtx, options: AwaitUpdateOptions<A, B>) => Effect.Effect<B, unknown, never>;
114
+
115
+ /** The `ctx.store` slice awaitUpdate needs, typed against the real
116
+ * `QueryDescriptor` so a full `AppContext` store (`DataStore`) is assignable —
117
+ * i.e. so the documented `awaitUpdate(ctx, …)` call inside an executor
118
+ * typechecks. Mirrors `AwaitSignalCtx`. */
119
+ export declare interface AwaitUpdateCtx {
120
+ readonly store: {
121
+ readonly query: (descriptor: QueryDescriptor) => Promise<ReadonlyArray<unknown>>;
122
+ };
123
+ }
124
+
125
+ export declare interface AwaitUpdateOptions<A, B = A> {
126
+ readonly name: string;
127
+ readonly schema: Schema.Schema<A, any>;
128
+ readonly success?: Schema.Schema<B, any>;
129
+ readonly handle?: (payload: A) => Effect.Effect<B, unknown, never>;
130
+ readonly pollIntervalMs?: number;
131
+ readonly maxPollIntervalMs?: number;
132
+ readonly timeoutMs?: number;
133
+ }
134
+
135
+ export declare const closeWorkflowChildrenForParent: (options: CloseWorkflowChildrenForParentOptions) => Promise<ReadonlyArray<WorkflowParentCloseDecision>>;
136
+
137
+ export declare interface CloseWorkflowChildrenForParentOptions {
138
+ readonly store: WorkflowParentCloseStore;
139
+ readonly parentExecutionId: string;
140
+ readonly parentStatus: WorkflowParentCloseStatus;
141
+ readonly recorder?: WorkflowRunRecorderService;
142
+ readonly emit?: WorkflowParentCloseEmitter['emit'];
143
+ readonly log?: WorkflowParentCloseLogger;
144
+ readonly limit?: number;
145
+ readonly interruptChild: (workflowName: string, executionId: string, policy: Exclude<WorkflowParentClosePolicy, 'abandon'>) => Promise<void>;
146
+ }
147
+
148
+ /**
149
+ * Externally complete a suspending signal wait, resuming the parked run with
150
+ * `payload`. The counterpart to {@link awaitSignalSuspending} and the
151
+ * suspending analogue of `sendWorkflowSignal`.
152
+ *
153
+ * Returns an `Effect` that requires the `WorkflowEngine` — run it through the
154
+ * same runner that executes workflows (the CLI's `workflowRuntime.runPromise`,
155
+ * i.e. the facade's `runEffect`), so `DurableDeferred.done` reaches the engine.
156
+ *
157
+ * Completing a deferred that no run is awaiting is a no-op, so it is safe to
158
+ * call this for every signal alongside `sendWorkflowSignal` — the polling
159
+ * `awaitSignal` reads the row, the suspending variant reads the deferred, and
160
+ * whichever the target workflow uses resumes.
161
+ */
162
+ export declare const completeSuspendingSignal: (input: CompleteSuspendingSignalInput) => Effect.Effect<{
163
+ readonly completed: boolean;
164
+ }, Cause.UnknownException, WorkflowEngine.WorkflowEngine>;
165
+
166
+ /** Input to {@link completeSuspendingSignal}. */
167
+ export declare interface CompleteSuspendingSignalInput {
168
+ /** Which run to resume. If it carries BOTH `executionId` and `workflowName`
169
+ * the store lookup is skipped; otherwise `store` is used to resolve them. */
170
+ readonly target: WorkflowMessageTarget;
171
+ /** Signal name — must match the workflow's `awaitSignalSuspending({ name })`. */
172
+ readonly signalName: string;
173
+ /** Payload delivered to the workflow. Stored raw and decoded on the awaiting
174
+ * side through that call's `schema`. */
175
+ readonly payload?: unknown;
176
+ /** Store used to resolve the run's `executionId` + workflow name when the
177
+ * target doesn't already provide both. */
178
+ readonly store?: WorkflowMessageStore;
179
+ }
180
+
181
+ /** FiberRef carrying the active workflow's executionId — set by
182
+ * `wrapWithRunRecording` on every workflow body entry, read by a
183
+ * child workflow's recorder to populate
184
+ * `_voltro_workflow_runs.parentExecutionId`. Distinct
185
+ * from `CurrentWorkflowRunId` (which is the runs-row primary key)
186
+ * because the natural durable id used by other workflows is the
187
+ * engine-assigned executionId, not our row id. */
188
+ export declare const CurrentWorkflowExecutionId: FiberRef.FiberRef<string | undefined>;
189
+
190
+ /** FiberRef carrying the active workflow run's id. The outer
191
+ * `wrapWithRunRecording` in dev.ts sets it via
192
+ * `Effect.locally(CurrentWorkflowRunId, runId)`. */
193
+ export declare const CurrentWorkflowRunId: FiberRef.FiberRef<string | undefined>;
194
+
195
+ export { durableClock }
196
+
197
+ /** Define a durable queue for concurrency-controlled side work. */
198
+ export declare const durableQueue: typeof durableQueueModule.make;
199
+
200
+ export { durableQueueModule }
201
+
202
+ export { durableRateLimiterModule }
203
+
204
+ /** Try to read the current workflow execution id. Returns undefined
205
+ * when called outside a recorded workflow body. */
206
+ export declare const getCurrentWorkflowExecutionId: () => Effect.Effect<string | undefined>;
207
+
208
+ /** Try to read the current run id. Returns undefined when not in a
209
+ * recorded workflow context (= step is being run outside the
210
+ * framework's wrapper, e.g. in a unit test). */
211
+ export declare const getCurrentWorkflowRunId: () => Effect.Effect<string | undefined>;
212
+
213
+ export declare const getWorkflowVersionMetadata: (value: unknown) => WorkflowVersionMetadata;
214
+
215
+ export declare interface InMemoryRecorder {
216
+ readonly layer: Layer.Layer<WorkflowRunRecorder>;
217
+ readonly readSteps: () => ReadonlyArray<RecordedStep>;
218
+ readonly readEvents: () => ReadonlyArray<RecordedEvent>;
219
+ }
220
+
221
+ export declare const inMemoryWorkflowEngineLayer: Layer.Layer<WorkflowEngine.WorkflowEngine>;
222
+
223
+ export declare interface InspectedStep {
224
+ readonly name: string;
225
+ /** The attempt number of THIS row (1 on first try, 2+ on retry). When
226
+ * a step has multiple attempts, the assembled step reflects the
227
+ * latest attempt's status/output/error. */
228
+ readonly attempt: number;
229
+ /** Total attempts recorded for this stepName (row count). */
230
+ readonly attempts: number;
231
+ readonly status: 'running' | 'succeeded' | 'failed';
232
+ readonly input: unknown;
233
+ readonly output: unknown;
234
+ readonly error: {
235
+ readonly tag: string | null;
236
+ readonly message: string;
237
+ } | null;
238
+ readonly durationMs: number | null;
239
+ }
240
+
241
+ export declare interface InspectedWorkflow {
242
+ readonly id: string;
243
+ readonly executionId: string;
244
+ readonly name: string;
245
+ readonly status: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'suspended';
246
+ readonly input: unknown;
247
+ readonly output: unknown;
248
+ readonly subject: Subject | null;
249
+ readonly source: string | null;
250
+ readonly steps: ReadonlyArray<InspectedStep>;
251
+ readonly startedAt: Date;
252
+ readonly finishedAt: Date | null;
253
+ readonly traceId: string | null;
254
+ readonly parentExecutionId: string | null;
255
+ readonly parentClosePolicy: WorkflowParentClosePolicy | null;
256
+ }
257
+
258
+ /** Minimal store surface `inspectWorkflow` needs: descriptor-in /
259
+ * rows-out. Satisfied by `FluentStore`, `DataStore`, and
260
+ * `InMemoryDataStore`. */
261
+ export declare interface InspectStore {
262
+ query(descriptor: QueryDescriptor): Promise<ReadonlyArray<Row>>;
263
+ }
264
+
265
+ /**
266
+ * Resolve a workflow run by its `id` OR its `executionId`, assemble the
267
+ * full inspected shape (run header + per-step attempts), and return it.
268
+ * Returns `null` when no run matches.
269
+ *
270
+ * @param idOrExecutionId `_voltro_workflow_runs.id` or `.executionId`.
271
+ * @param store any `query`-capable store (fluent `ctx.store`,
272
+ * `InMemoryDataStore`, or a live SQL `DataStore`).
273
+ */
274
+ export declare const inspectWorkflow: (idOrExecutionId: string, store: InspectStore) => Promise<InspectedWorkflow | null>;
275
+
276
+ export declare const isWorkflowWorkerLayer: (value: unknown) => value is WorkflowWorkerLayerBrand;
277
+
278
+ export declare const makeInMemoryRecorder: () => InMemoryRecorder;
279
+
280
+ export declare const makeWorkflowRunRecorder: (options: WorkflowRunRecorderOptions) => WorkflowRunRecorderService;
281
+
282
+ export declare const makeWorkflowUpdateId: () => string;
283
+
284
+ declare type NormaliseWorkflowMessages<M extends WorkflowMessageSchemas | undefined> = {
285
+ readonly signals: M extends {
286
+ readonly signals: infer Signals;
287
+ } ? NonNullable<Signals> & Readonly<Record<string, Schema.Schema.Any>> : {};
288
+ readonly updates: M extends {
289
+ readonly updates: infer Updates;
290
+ } ? NonNullable<Updates> & Readonly<Record<string, WorkflowMessagePairSchemas>> : {};
291
+ readonly queries: M extends {
292
+ readonly queries: infer Queries;
293
+ } ? NonNullable<Queries> & Readonly<Record<string, WorkflowMessagePairSchemas>> : {};
294
+ };
295
+
296
+ /** Enqueue work and await the durable queue worker's result. */
297
+ export declare const processQueue: typeof durableQueueModule.process;
298
+
299
+ /** Start a durable queue worker layer with bounded concurrency.
300
+ *
301
+ * `DurableQueue.worker` is itself generic (`<Payload, Success, Error, R>`), so
302
+ * a hand-written wrapper can't re-state that generic signature — the body runs
303
+ * against erased args and the const's explicit `: typeof DurableQueue.worker`
304
+ * annotation restores the full generic public type. That annotation (not an
305
+ * inferred type) is what emits in the `.d.ts`, so it stays portable. */
306
+ export declare const queueWorker: typeof durableQueueModule.worker;
307
+
308
+ /** Durable rate limiter activity. Delays through the workflow clock. */
309
+ export declare const rateLimit: typeof durableRateLimiterModule.rateLimit;
310
+
311
+ /** One recorded run-event — mirrors a `_voltro_workflow_run_events` row. */
312
+ export declare interface RecordedEvent {
313
+ readonly id: string;
314
+ readonly runId: string;
315
+ readonly eventType: WorkflowRunEventType;
316
+ readonly payload: Record<string, unknown> | null;
317
+ readonly stepName: string | null;
318
+ readonly attempt: number | null;
319
+ readonly occurredAt: Date;
320
+ }
321
+
322
+ /** One recorded step attempt — mirrors a `_voltro_workflow_run_steps`
323
+ * row closely enough for inspection. One row per attempt. */
324
+ export declare interface RecordedStep {
325
+ readonly id: string;
326
+ readonly runId: string;
327
+ readonly stepName: string;
328
+ readonly attempt: number;
329
+ status: 'running' | 'succeeded' | 'failed';
330
+ readonly input: unknown;
331
+ readonly retryPolicy: StepRetryPolicy | null;
332
+ output: unknown;
333
+ errorTag: string | null;
334
+ errorMessage: string | null;
335
+ errorCause: {
336
+ readonly pretty: string;
337
+ readonly failures: ReadonlyArray<unknown>;
338
+ readonly defects: ReadonlyArray<unknown>;
339
+ } | null;
340
+ readonly startedAt: Date;
341
+ completedAt: Date | null;
342
+ durationMs: number | null;
343
+ }
344
+
345
+ export declare const resolveWorkflowMessageRun: (store: WorkflowMessageStore, target: WorkflowMessageTarget) => Promise<WorkflowResolvedRun>;
346
+
347
+ export declare const sendWorkflowSignal: (input: {
348
+ readonly store: WorkflowMessageStore;
349
+ readonly recorder: WorkflowRunRecorderService;
350
+ readonly target: WorkflowMessageTarget;
351
+ readonly signalName: string;
352
+ readonly payload?: unknown;
353
+ }) => Promise<{
354
+ readonly eventId: string;
355
+ }>;
356
+
357
+ export declare const sendWorkflowUpdate: (input: {
358
+ readonly store: WorkflowMessageStore;
359
+ readonly recorder: WorkflowRunRecorderService;
360
+ readonly target: WorkflowMessageTarget;
361
+ readonly updateName: string;
362
+ readonly payload?: unknown;
363
+ readonly options?: WorkflowUpdateSendOptions;
364
+ }) => Promise<WorkflowUpdateResult>;
365
+
366
+ export declare const serialiseWorkflowRowForWire: (row: Record<string, unknown>) => Record<string, unknown>;
367
+
368
+ /**
369
+ * Durable sleep — wake time journaled in the cluster. Wrapper around
370
+ * `DurableClock.sleep` that emits `timer-set` / `timer-fired` events
371
+ * to the recorder so the dashboard's Gantt timeline renders the wait
372
+ * as a visible bar — a workflow waiting on a cron-style delay should
373
+ * not look like a black hole.
374
+ *
375
+ * The wrap is transparent: signature matches `DurableClock.sleep`,
376
+ * no observable behavior change beyond the event emissions. When no
377
+ * recorder is in scope (unit tests, no `voltro dev`/`voltro start`
378
+ * wrapper around the workflow) it's a zero-overhead passthrough.
379
+ */
380
+ export declare const sleep: typeof durableClock.sleep;
381
+
382
+ export declare const step: <R, Success extends Schema.Schema.Any = typeof Schema.Void, Error extends Schema.Schema.All = typeof Schema.Never>(options: StepOptions<R, Success, Error>) => ReturnType<typeof stepModule.make<R, Success, Error>>;
383
+
384
+ /**
385
+ * Derive a deterministic idempotency key for the current workflow run,
386
+ * scoped to `name`. Call it inside a `step()` so a retried external
387
+ * side-effect converges on a single key instead of double-creating —
388
+ * pair it with a read-before-write provider-operations ledger. Pass
389
+ * `{ includeAttempt: true }` to vary the key per attempt.
390
+ *
391
+ * Named for the framework's `step` vocabulary (it is upstream's
392
+ * `Activity.idempotencyKey`).
393
+ */
394
+ export declare const stepIdempotencyKey: typeof stepModule.idempotencyKey;
395
+
396
+ export { stepModule }
397
+
398
+ /**
399
+ * Define a checkpointed step (activity) inside a workflow body.
400
+ *
401
+ * Wraps `@effect/workflow`'s `Activity.make` to record every
402
+ * invocation in `_voltro_workflow_run_steps`. The wrapping is
403
+ * transparent — accepts everything `Activity.make` accepts, plus an
404
+ * optional `input` field whose value is persisted to the step row
405
+ * (truncated to 8 KB by the recorder). The dashboard surfaces it as
406
+ * "Input" alongside Output, so operators can answer "what arguments
407
+ * did this step receive?" without grep-ing logs.
408
+ *
409
+ * ```ts
410
+ * step({
411
+ * name: 'fetch-todos',
412
+ * input: { tenantId, since: lastFetch }, // ← optional, recorded
413
+ * execute: Effect.tryPromise({ try: () => ctx.store.query(...) }),
414
+ * })
415
+ * ```
416
+ *
417
+ * Without the recorder service in scope (unit tests, etc.) the input
418
+ * is dropped and the wrap is a no-op.
419
+ */
420
+ declare type StepOptions<R, Success extends Schema.Schema.Any, Error extends Schema.Schema.All> = {
421
+ readonly name: string;
422
+ readonly success?: Success | undefined;
423
+ readonly error?: Error | undefined;
424
+ readonly execute: Effect.Effect<Success['Type'], Error['Type'], R>;
425
+ readonly interruptRetryPolicy?: Parameters<typeof stepModule.make<R, Success, Error>>[0]['interruptRetryPolicy'];
426
+ /** Structured value recorded into `_voltro_workflow_run_steps.input`.
427
+ * Pass whatever makes the step debuggable in isolation. */
428
+ readonly input?: unknown;
429
+ /** Declarative retry-policy summary recorded into
430
+ * `_voltro_workflow_run_steps.retryPolicy`. Pure metadata — does
431
+ * NOT change retry behavior on its own. Wire actual retries via
432
+ * `Effect.retry` inside `execute:` or via `interruptRetryPolicy`
433
+ * above; this field tells the dashboard what the intended policy
434
+ * looks like so the per-attempt panel can show "attempt 2 of 5,
435
+ * exponential 1s base" instead of just "attempt 2". */
436
+ readonly retry?: StepRetryPolicy;
437
+ };
438
+
439
+ /**
440
+ * Declarative retry-policy summary. Pure metadata for the dashboard;
441
+ * the framework does NOT apply this. Users wire actual retry behavior
442
+ * via `Effect.retry(Schedule.*)` inside `execute:` or
443
+ * `Activity.make`'s `interruptRetryPolicy`.
444
+ *
445
+ * Shape is intentionally close to Effect's Schedule combinators so a
446
+ * future framework version can auto-translate declarative policies
447
+ * into a Schedule + apply it. For now: record + display.
448
+ *
449
+ * { strategy: 'exponential', maxAttempts: 5, baseDelay: '1 second', maxDelay: '30 seconds' }
450
+ * { strategy: 'fixed', maxAttempts: 3, baseDelay: '500 millis' }
451
+ * { strategy: 'linear', maxAttempts: 4, baseDelay: '1 second', step: '2 seconds' }
452
+ */
453
+ export declare interface StepRetryPolicy {
454
+ readonly strategy: 'exponential' | 'fixed' | 'linear';
455
+ readonly maxAttempts: number;
456
+ /** Effect Duration string, e.g. `'1 second'`, `'500 millis'`. */
457
+ readonly baseDelay?: string;
458
+ /** Cap on exponential / linear growth, e.g. `'30 seconds'`. */
459
+ readonly maxDelay?: string;
460
+ /** Increment for `linear` strategy. */
461
+ readonly step?: string;
462
+ /** Free-form note rendered as a chip in the dashboard. Useful when
463
+ * the retry behavior is partially implemented in the user's
464
+ * Effect chain and the summary is just documentation. */
465
+ readonly note?: string;
466
+ }
467
+
468
+ /**
469
+ * The stable `DurableDeferred` name for a suspending signal wait. It is a pure
470
+ * function of the workflow name + the signal name, so the awaiting body and an
471
+ * external completer derive the SAME name without any shared state — the
472
+ * engine keys the deferred result by `<executionId>/<this name>`.
473
+ */
474
+ export declare const suspendingSignalDeferredName: (workflowName: string, signalName: string) => string;
475
+
476
+ export declare const truncateWorkflowValue: (value: unknown, bytes?: number) => unknown;
477
+
478
+ export declare const _voltroWorkflowRunEventsTable: SchemaTable;
479
+
480
+ export declare const _voltroWorkflowRunsTable: SchemaTable;
481
+
482
+ export declare const _voltroWorkflowRunStepsTable: SchemaTable;
483
+
484
+ export declare const _voltroWorkflowStartContextsTable: TableLike;
485
+
486
+ /**
487
+ * Add compensating (rollback) logic to a top-level effect in a workflow
488
+ * body. The finalizer runs if the WHOLE workflow later fails — use it for
489
+ * saga-style undo: release a half-acquired resource, revert a partial
490
+ * promotion. Upstream constraint: compensation is registered only for
491
+ * top-level effects in the workflow, NOT for nested steps/activities.
492
+ *
493
+ * Pinned with `typeof` to keep the emitted .d.ts portable (see the TS2742
494
+ * note above the type-explicit re-exports).
495
+ *
496
+ * ```ts
497
+ * yield* withCompensation(
498
+ * step({ name: 'provision-db', execute: ... }),
499
+ * (resource) => releaseResource(resource), // runs only if the workflow fails
500
+ * )
501
+ * ```
502
+ */
503
+ export declare const withCompensation: typeof workflowModule.withCompensation;
504
+
505
+ /** Define a durable workflow. Adds Voltro version metadata on top of `Workflow.make`. */
506
+ export declare const workflow: WorkflowFn;
507
+
508
+ export declare interface WorkflowExecuteRecordingOptions extends WorkflowRunRecorderOptions {
509
+ readonly name: string;
510
+ readonly subject?: unknown;
511
+ readonly source?: string | null;
512
+ readonly traceId?: string | null;
513
+ readonly workflowVersion?: string | null;
514
+ readonly workflowCompatibleVersions?: ReadonlyArray<string> | null;
515
+ readonly workflowPatches?: ReadonlyArray<string> | null;
516
+ readonly parentExecutionId?: string | null;
517
+ readonly parentClosePolicy?: WorkflowParentClosePolicy | null;
518
+ readonly onParentClose?: (input: {
519
+ readonly workflowName: string;
520
+ readonly executionId: string;
521
+ readonly runId?: string;
522
+ readonly status: WorkflowParentCloseStatus;
523
+ }) => Promise<void>;
524
+ readonly recorder?: WorkflowRunRecorderService;
525
+ readonly pluginWorkflowStepLayer?: Layer.Layer<any, never, never>;
526
+ }
527
+
528
+ /** Callable shape of `workflow`: the raw `Workflow.make` surface PLUS the
529
+ * Voltro-typed overload that threads the message-schema metadata onto the
530
+ * returned definition. Explicit alias so the `.d.ts` names this type by its
531
+ * own alias — the inferred `Workflow.make` type references internal `@effect`
532
+ * modules via `.pnpm` paths and is not portable (TS2742) when re-emitted. */
533
+ declare type WorkflowFn = WorkflowMake & (<const Options extends WorkflowOptions>(options: Options) => ReturnType<WorkflowMake> & WorkflowMessagesCarrier<NormaliseWorkflowMessages<Options['messages']>>);
534
+
535
+ declare type WorkflowMake = typeof workflowModule.make;
536
+
537
+ export declare interface WorkflowMessagePairSchemas {
538
+ readonly payload: Schema.Schema.Any;
539
+ readonly success: Schema.Schema.Any;
540
+ }
541
+
542
+ export declare interface WorkflowMessagesCarrier<M extends WorkflowMessagesMetadata = WorkflowMessagesMetadata> {
543
+ readonly [WorkflowMessagesProperty]: M;
544
+ }
545
+
546
+ export declare interface WorkflowMessageSchemas {
547
+ readonly signals?: Readonly<Record<string, Schema.Schema.Any>>;
548
+ readonly updates?: Readonly<Record<string, WorkflowMessagePairSchemas>>;
549
+ readonly queries?: Readonly<Record<string, WorkflowMessagePairSchemas>>;
550
+ }
551
+
552
+ export declare interface WorkflowMessagesMetadata {
553
+ readonly signals: Readonly<Record<string, Schema.Schema.Any>>;
554
+ readonly updates: Readonly<Record<string, WorkflowMessagePairSchemas>>;
555
+ readonly queries: Readonly<Record<string, WorkflowMessagePairSchemas>>;
556
+ }
557
+
558
+ export declare const WorkflowMessagesProperty = "__voltroWorkflowMessages";
559
+
560
+ export declare interface WorkflowMessageStore {
561
+ readonly query: (input: QueryDescriptor) => Promise<ReadonlyArray<unknown>>;
562
+ }
563
+
564
+ export declare interface WorkflowMessageTarget {
565
+ readonly id?: string;
566
+ readonly executionId?: string;
567
+ /** Workflow name (the `_voltro_workflow_runs.tag` column). Lets a caller
568
+ * that already holds it (e.g. from a `WorkflowRunHandle`) skip the store
569
+ * lookup when completing a suspending signal — see
570
+ * `completeSuspendingSignal`. */
571
+ readonly workflowName?: string;
572
+ }
573
+
574
+ export { workflowModule }
575
+
576
+ declare type WorkflowOptions = Parameters<WorkflowMake>[0] & WorkflowVersionOptions & {
577
+ readonly messages?: WorkflowMessageSchemas;
578
+ };
579
+
580
+ export declare interface WorkflowParentCloseDecision {
581
+ readonly runId: string;
582
+ readonly workflowName: string;
583
+ readonly executionId: string;
584
+ readonly policy: WorkflowParentClosePolicy | null;
585
+ readonly action: 'cancelled' | 'abandoned' | 'skipped' | 'failed';
586
+ readonly reason?: string;
587
+ }
588
+
589
+ export declare interface WorkflowParentCloseEmitter {
590
+ emit(channel: 'workflowRuns' | 'workflowSteps' | 'workflowEvents', event: {
591
+ readonly op: 'insert' | 'update';
592
+ readonly row: Record<string, unknown>;
593
+ }): void;
594
+ }
595
+
596
+ export declare interface WorkflowParentCloseLogger {
597
+ warn(message: string, fields?: Record<string, unknown>): void;
598
+ }
599
+
600
+ export declare type WorkflowParentCloseStatus = 'succeeded' | 'failed' | 'cancelled';
601
+
602
+ export declare interface WorkflowParentCloseStore {
603
+ query(descriptor: QueryDescriptor): Promise<ReadonlyArray<unknown>>;
604
+ update(table: string, id: string, patch: unknown): Promise<unknown | null>;
605
+ }
606
+
607
+ export declare interface WorkflowRecordingEmitter {
608
+ emit(channel: 'workflowRuns' | 'workflowSteps' | 'workflowEvents', event: {
609
+ readonly op: 'insert' | 'update';
610
+ readonly row: Record<string, unknown>;
611
+ }): void;
612
+ }
613
+
614
+ export declare interface WorkflowRecordingLogger {
615
+ warn(message: string, fields?: Record<string, unknown>): void;
616
+ }
617
+
618
+ export declare interface WorkflowRecordingStore {
619
+ insert(table: string, row: unknown): Promise<unknown>;
620
+ update(table: string, id: string, patch: unknown): Promise<unknown | null>;
621
+ query?(descriptor: unknown): Promise<ReadonlyArray<unknown>>;
622
+ }
623
+
624
+ export declare interface WorkflowResolvedRun {
625
+ readonly id: string;
626
+ readonly executionId?: string;
627
+ /** Denormalised workflow name (`_voltro_workflow_runs.tag`). Present because
628
+ * the resolve query projects all columns; used to key a suspending signal's
629
+ * `DurableDeferred` by workflow name + signal name. */
630
+ readonly tag?: string;
631
+ }
632
+
633
+ /**
634
+ * Closed enumeration of event types the framework knows how to emit.
635
+ * Kept here (not in `runEventsTable.ts`) so callers in the recorder
636
+ * + dev.ts share a single source of truth. The TABLE column is
637
+ * open-text on purpose (future phases add new types without a column
638
+ * migration) — but at emit time we still want type-safety.
639
+ */
640
+ export declare type WorkflowRunEventType = 'run-started' | 'run-suspended' | 'run-resumed' | 'run-cancelled' | 'run-succeeded' | 'run-failed'
641
+ /** Emitted when `sleep({ name, duration })` enters the wait — `payload`
642
+ * carries `{ name, durationMs, scheduledWakeAt }`. The dashboard
643
+ * Gantt renders these as a hatched bar in the timeline so a
644
+ * workflow waiting on a cron-style delay isn't shown as silently
645
+ * hung. */
646
+ | 'timer-set'
647
+ /** Emitted when the sleep wakes — `payload` carries `{ name,
648
+ * actualDurationMs }`. Pairs with the matching `timer-set` on the
649
+ * same `name`. */
650
+ | 'timer-fired'
651
+ /** Emitted by `awaitSignal({ name, schema })` when the
652
+ * workflow enters the wait. `payload` carries `{ signalName }`. The
653
+ * dashboard surfaces the awaited signal in the run header + adds a
654
+ * "Send signal" CTA targeting this name. */
655
+ | 'signal-awaited'
656
+ /** Emitted by the inspect bus when an external caller
657
+ * sends a signal via `POST /_voltro/inspect/workflows/runs/:id/signal`.
658
+ * `payload` carries `{ signalName, value }`. The polling
659
+ * `awaitSignal` activity picks the event up on its next tick. */
660
+ | 'signal-sent'
661
+ /** Emitted by `awaitSignal` after a matching
662
+ * `signal-sent` is observed and parsed. `payload` carries
663
+ * `{ signalName, value }`. Pairs visually with `signal-awaited` for
664
+ * the same signalName so timelines render a complete handshake. */
665
+ | 'signal-received'
666
+ /** Emitted when an external caller sends a synchronous workflow
667
+ * update. `payload` carries `{ updateId, updateName, value }`.
668
+ * The matching `awaitUpdate(...)` activity picks it up, validates
669
+ * it, and records a terminal update event. */
670
+ | 'update-requested'
671
+ /** Emitted by `awaitUpdate(...)` once the workflow has claimed an
672
+ * update request. Useful in dashboards to distinguish "queued for
673
+ * workflow" from "currently being handled". */
674
+ | 'update-received'
675
+ /** Emitted by `awaitUpdate(...)` after the handler returns.
676
+ * `payload` carries `{ updateId, updateName, result }`; the
677
+ * caller's `ctx.workflows.update(...)` waits for this event. */
678
+ | 'update-completed'
679
+ /** Emitted by `awaitUpdate(...)` when schema validation or the
680
+ * update handler fails. `payload` carries `{ updateId, updateName,
681
+ * errorTag, errorMessage }`; the caller's update promise rejects. */
682
+ | 'update-failed';
683
+
684
+ export declare class WorkflowRunRecorder extends WorkflowRunRecorder_base {
685
+ }
686
+
687
+ declare const WorkflowRunRecorder_base: Context.TagClass<WorkflowRunRecorder, "@voltro/WorkflowRunRecorder", WorkflowRunRecorderService>;
688
+
689
+ export declare interface WorkflowRunRecorderOptions {
690
+ readonly store: WorkflowRecordingStore;
691
+ readonly emit?: WorkflowRecordingEmitter['emit'];
692
+ readonly log?: WorkflowRecordingLogger;
693
+ readonly truncateBytes?: number;
694
+ readonly makeId?: (prefix: 'wr' | 'ws' | 'wfev') => string;
695
+ /** Dormancy wakeup producer — present only in sleep mode. Absent →
696
+ * zero overhead (always-on default). */
697
+ readonly wakeups?: WorkflowWakeupHook;
698
+ }
699
+
700
+ /** Per-step lifecycle hooks. Implementations should not throw —
701
+ * recorder failures must NOT break workflow execution.
702
+ *
703
+ * Two responsibilities: write rows to the steps table AND emit
704
+ * inspect-stream events so cross-process consumers (cloud-api's
705
+ * per-app SSE bridge → cache mirror → dashboard subscriptions) see
706
+ * state transitions in real time. */
707
+ export declare interface WorkflowRunRecorderService {
708
+ /** Called before a step body runs. Returns an id the caller passes
709
+ * back to `endStep`. Implementations may return undefined to
710
+ * signal "not recording this step" (e.g. no current run id, or
711
+ * the runs table isn't available yet); callers should treat that
712
+ * as "no recording happened" and not call `endStep`. */
713
+ readonly startStep: (input: {
714
+ readonly runId: string;
715
+ readonly stepName: string;
716
+ readonly attempt: number;
717
+ /** User-declared structured input recorded into
718
+ * `_voltro_workflow_run_steps.input`. The recorder truncates large
719
+ * payloads — pass whatever the step needs to make sense in
720
+ * isolation, not the full universe. */
721
+ readonly stepInput?: unknown;
722
+ /** Declarative retry-policy summary recorded into
723
+ * `_voltro_workflow_run_steps.retryPolicy`. Pure metadata — the
724
+ * framework doesn't apply this; the user is still responsible for
725
+ * using `Effect.retry` or `Activity.interruptRetryPolicy` to
726
+ * implement the actual retry behavior. The dashboard renders the
727
+ * policy alongside the attempt counter so operators can see
728
+ * "attempt 2 of 5, exponential 1s base" at a glance. */
729
+ readonly retryPolicy?: StepRetryPolicy;
730
+ }) => Promise<string | undefined>;
731
+ readonly endStepSuccess: (input: {
732
+ readonly stepRecordId: string;
733
+ readonly output: unknown;
734
+ readonly durationMs: number;
735
+ }) => Promise<void>;
736
+ readonly endStepFailure: (input: {
737
+ readonly stepRecordId: string;
738
+ readonly errorTag: string | null;
739
+ readonly errorMessage: string;
740
+ /** Structured Cause record — pretty form +
741
+ * typed failures + defects. The dashboard renders the pretty
742
+ * string as a stack-trace panel in the expanded step view, so
743
+ * ops can see "where in user code" the throw originated. */
744
+ readonly errorCause?: {
745
+ readonly pretty: string;
746
+ readonly failures: ReadonlyArray<unknown>;
747
+ readonly defects: ReadonlyArray<unknown>;
748
+ };
749
+ readonly durationMs: number;
750
+ }) => Promise<void>;
751
+ /**
752
+ * Record a run-scoped lifecycle event into
753
+ * `_voltro_workflow_run_events`. Used for transitions that don't
754
+ * have a row representation in the runs or steps tables:
755
+ * suspend / resume / cancel, and (in later phases) timer fires +
756
+ * signal receipts + child spawns. Implementations should NOT throw —
757
+ * recorder failures must NOT break workflow execution.
758
+ */
759
+ readonly recordEvent: (input: {
760
+ readonly runId: string;
761
+ readonly eventType: WorkflowRunEventType;
762
+ readonly payload?: Record<string, unknown> | null;
763
+ readonly stepName?: string | null;
764
+ readonly attempt?: number | null;
765
+ }) => Promise<{
766
+ readonly id: string;
767
+ }>;
768
+ }
769
+
770
+ /**
771
+ * Per-step plugin interceptor. Plugins can wrap every `step()`
772
+ * (== `Activity.make`) invocation with observability, suppress /
773
+ * substitute behaviour, or retry-policy overrides. Composed across
774
+ * plugins by the CLI (`composePluginWorkflowStep`).
775
+ *
776
+ * Receives the user's `execute` Effect + a `WorkflowStepContext`
777
+ * carrying `{ runId, stepName, attempt }`. Returns a wrapped Effect.
778
+ * A FAIL inside the interceptor abort the step the same way a fail
779
+ * in the user effect would — including triggering @effect/workflow's
780
+ * retry policy.
781
+ */
782
+ export declare interface WorkflowStepContext {
783
+ /** Run id from `_voltro_workflow_runs` (or 'unrecorded'). */
784
+ readonly runId: string;
785
+ /** Step name (== `Activity.make({ name })`). */
786
+ readonly stepName: string;
787
+ /** Attempt number (1 on first try, 2+ on retry). */
788
+ readonly attempt: number;
789
+ }
790
+
791
+ export declare type WorkflowStepInterceptor = <Success, ErrorE, R>(next: Effect.Effect<Success, ErrorE, R>, ctx: WorkflowStepContext) => Effect.Effect<Success, ErrorE, R>;
792
+
793
+ export declare interface WorkflowStepInterceptorService {
794
+ readonly interceptor: WorkflowStepInterceptor;
795
+ }
796
+
797
+ /**
798
+ * Optional service Tag carrying the composed per-step interceptor
799
+ * chain. When absent (no plugin contributes `onWorkflowStep`) the
800
+ * step wrapper takes the zero-overhead unchanged path.
801
+ */
802
+ export declare class WorkflowStepInterceptorTag extends WorkflowStepInterceptorTag_base {
803
+ }
804
+
805
+ declare const WorkflowStepInterceptorTag_base: Context.TagClass<WorkflowStepInterceptorTag, "@voltro/WorkflowStepInterceptor", WorkflowStepInterceptorService>;
806
+
807
+ export declare interface WorkflowUpdateResult {
808
+ readonly eventId: string;
809
+ readonly updateId: string;
810
+ readonly completedEventId: string;
811
+ readonly result: unknown;
812
+ }
813
+
814
+ export declare interface WorkflowUpdateSendOptions {
815
+ readonly timeoutMs?: number;
816
+ readonly pollIntervalMs?: number;
817
+ }
818
+
819
+ export declare interface WorkflowVersionMetadata {
820
+ readonly version: string;
821
+ readonly compatibleWith: ReadonlyArray<string>;
822
+ readonly patches: ReadonlyArray<string>;
823
+ }
824
+
825
+ export declare interface WorkflowVersionOptions {
826
+ readonly version?: string | number;
827
+ readonly compatibleWith?: ReadonlyArray<string | number>;
828
+ readonly patches?: ReadonlyArray<string>;
829
+ }
830
+
831
+ export declare const WorkflowVersionTypeId: unique symbol;
832
+
833
+ /**
834
+ * Dormancy producer (plan 50 — scale-to-zero). Injected by the CLI ONLY
835
+ * in `dormancy: 'sleep'` mode so the workflow package stays free of a
836
+ * `@voltro/runtime` dependency (it has none). The recorder mirrors a
837
+ * durable-clock wait into the wakeup registry: `register` on `timer-set`,
838
+ * `cancel` when the wait ends or the run reaches a terminal state. The
839
+ * external waker reads those rows to resume a scaled-to-zero runner.
840
+ *
841
+ * This NEVER touches `DurableClock.sleep` or the cluster journal — it is
842
+ * additive bookkeeping next to the existing best-effort event write, so
843
+ * the durable-resume path is unaffected. Implementations must not throw.
844
+ */
845
+ export declare interface WorkflowWakeupHook {
846
+ /** `tenantId` is the wake-routing target carried from the workflow's
847
+ * caller subject (plan 50). Absent → the CLI hook falls back to the
848
+ * deployment's configured tenant (correct for single-tenant). */
849
+ readonly register: (runId: string, wakeAt: Date, tenantId?: string) => Promise<void>;
850
+ /** `tenantId` is carried on the `timer-fired` event (the normal cancel
851
+ * path) so the cancel targets the same row `register` created. Absent
852
+ * on terminal-event cancels → the CLI hook falls back to the
853
+ * deployment tenant (consistent with register for single-tenant). */
854
+ readonly cancel: (runId: string, tenantId?: string) => Promise<void>;
855
+ }
856
+
857
+ export declare interface WorkflowWorkerLayerBrand {
858
+ readonly [WorkflowWorkerLayerTypeId]: true;
859
+ }
860
+
861
+ export declare const WorkflowWorkerLayerTypeId: unique symbol;
862
+
863
+ export declare const wrapWorkflowExecuteWithRunRecording: (options: WorkflowExecuteRecordingOptions, userExecute: (payload: unknown, executionId: string) => Effect.Effect<unknown, unknown, never>) => (payload: unknown, executionId: string) => Effect.Effect<unknown, unknown, never>;
864
+
865
+ export { }