@jagreehal/workflow 1.0.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,775 @@
1
+ import { Result, ErrorOf, CauseOf, UnexpectedError, WorkflowEvent, StepFailureMeta, RunStep, AsyncResult } from './core.cjs';
2
+ export { StepOptions } from './core.cjs';
3
+
4
+ /**
5
+ * @jagreehal/workflow/workflow
6
+ *
7
+ * Workflow orchestration with createWorkflow.
8
+ * Use this for typed async workflows with automatic error inference.
9
+ */
10
+
11
+ /**
12
+ * Interface for step result caching.
13
+ * Implement this interface to provide custom caching strategies.
14
+ * A simple Map<string, Result> works for in-memory caching.
15
+ *
16
+ * Note: Cache stores Result<unknown, unknown, unknown> because different steps
17
+ * have different value/error/cause types. The actual runtime values are preserved;
18
+ * only the static types are widened. For error results, the cause value is encoded
19
+ * in CachedErrorCause to preserve metadata for proper replay.
20
+ *
21
+ * @example
22
+ * // Simple in-memory cache
23
+ * const cache = new Map<string, Result<unknown, unknown, unknown>>();
24
+ *
25
+ * // Or implement custom cache with TTL, LRU, etc.
26
+ * const cache: StepCache = {
27
+ * get: (key) => myCache.get(key),
28
+ * set: (key, result) => myCache.set(key, result, { ttl: 60000 }),
29
+ * has: (key) => myCache.has(key),
30
+ * delete: (key) => myCache.delete(key),
31
+ * clear: () => myCache.clear(),
32
+ * };
33
+ */
34
+ interface StepCache {
35
+ get(key: string): Result<unknown, unknown, unknown> | undefined;
36
+ set(key: string, result: Result<unknown, unknown, unknown>): void;
37
+ has(key: string): boolean;
38
+ delete(key: string): boolean;
39
+ clear(): void;
40
+ }
41
+ /**
42
+ * Entry for a saved step result with optional metadata.
43
+ * The meta field preserves origin information for proper replay.
44
+ */
45
+ interface ResumeStateEntry {
46
+ result: Result<unknown, unknown, unknown>;
47
+ /** Optional metadata for error origin (from step_complete event) */
48
+ meta?: StepFailureMeta;
49
+ }
50
+ /**
51
+ * Resume state for workflow replay.
52
+ * Pre-populate step results to skip execution on resume.
53
+ *
54
+ * Note: When saving to persistent storage, you may need custom serialization
55
+ * for complex cause types. JSON.stringify works for simple values, but Error
56
+ * objects and other non-plain types require special handling.
57
+ *
58
+ * @example
59
+ * // Collect from step_complete events using the helper
60
+ * const collector = createStepCollector();
61
+ * const workflow = createWorkflow({ fetchUser }, {
62
+ * onEvent: collector.handleEvent,
63
+ * });
64
+ * // Later: collector.getState() returns ResumeState
65
+ *
66
+ * @example
67
+ * // Resume with saved state
68
+ * const workflow = createWorkflow({ fetchUser }, {
69
+ * resumeState: { steps: savedSteps }
70
+ * });
71
+ */
72
+ interface ResumeState {
73
+ /** Map of step keys to their cached results with optional metadata */
74
+ steps: Map<string, ResumeStateEntry>;
75
+ }
76
+ /**
77
+ * Create a collector for step results to build resume state.
78
+ *
79
+ * ## When to Use
80
+ *
81
+ * Use `createStepCollector` when you need to:
82
+ * - **Save workflow state** for later replay/resume
83
+ * - **Persist step results** to a database or file system
84
+ * - **Build resume state** from workflow execution
85
+ * - **Enable workflow replay** after application restarts
86
+ *
87
+ * ## Why Use This Instead of Manual Collection
88
+ *
89
+ * - **Automatic filtering**: Only collects `step_complete` events (ignores other events)
90
+ * - **Metadata preservation**: Captures both result and meta for proper error replay
91
+ * - **Type-safe**: Returns properly typed `ResumeState`
92
+ * - **Convenient API**: Simple `handleEvent` → `getState` pattern
93
+ *
94
+ * ## How It Works
95
+ *
96
+ * 1. Create collector and pass `handleEvent` to workflow's `onEvent` option
97
+ * 2. Workflow emits `step_complete` events for keyed steps
98
+ * 3. Collector automatically captures these events
99
+ * 4. Call `getState()` to get the collected `ResumeState`
100
+ * 5. Persist state (e.g., to database) for later resume
101
+ *
102
+ * ## Important Notes
103
+ *
104
+ * - Only steps with a `key` option are collected (unkeyed steps are not saved)
105
+ * - The collector preserves error metadata for proper replay behavior
106
+ * - State can be serialized to JSON (but complex cause types may need custom handling)
107
+ *
108
+ * @returns An object with:
109
+ * - `handleEvent`: Function to pass to workflow's `onEvent` option
110
+ * - `getState`: Get collected resume state (call after workflow execution)
111
+ * - `clear`: Clear all collected state
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // Collect state during workflow execution
116
+ * const collector = createStepCollector();
117
+ *
118
+ * const workflow = createWorkflow({ fetchUser, fetchPosts }, {
119
+ * onEvent: collector.handleEvent, // Pass collector's handler
120
+ * });
121
+ *
122
+ * await workflow(async (step) => {
123
+ * // Only keyed steps are collected
124
+ * const user = await step(() => fetchUser("1"), { key: "user:1" });
125
+ * const posts = await step(() => fetchPosts(user.id), { key: `posts:${user.id}` });
126
+ * return { user, posts };
127
+ * });
128
+ *
129
+ * // Get collected state for persistence
130
+ * const state = collector.getState();
131
+ * // state.steps contains: 'user:1' and 'posts:1' entries
132
+ *
133
+ * // Save to database
134
+ * await db.saveWorkflowState(workflowId, state);
135
+ * ```
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * // Resume workflow from saved state
140
+ * const savedState = await db.loadWorkflowState(workflowId);
141
+ * const workflow = createWorkflow({ fetchUser, fetchPosts }, {
142
+ * resumeState: savedState // Pre-populate cache from saved state
143
+ * });
144
+ *
145
+ * // Cached steps skip execution, new steps run normally
146
+ * await workflow(async (step) => {
147
+ * const user = await step(() => fetchUser("1"), { key: "user:1" }); // Cache hit
148
+ * const posts = await step(() => fetchPosts(user.id), { key: `posts:${user.id}` }); // Cache hit
149
+ * return { user, posts };
150
+ * });
151
+ * ```
152
+ */
153
+ declare function createStepCollector(): {
154
+ handleEvent: (event: WorkflowEvent<unknown>) => void;
155
+ getState: () => ResumeState;
156
+ clear: () => void;
157
+ };
158
+ /**
159
+ * Constraint for Result-returning functions
160
+ * Used by createWorkflow to ensure only valid functions are passed
161
+ */
162
+ type AnyResultFn = (...args: any[]) => Result<any, any, any> | Promise<Result<any, any, any>>;
163
+ /**
164
+ * Extract union of error types from a deps object
165
+ * Example: ErrorsOfDeps<{ fetchUser: typeof fetchUser, fetchPosts: typeof fetchPosts }>
166
+ * yields: 'NOT_FOUND' | 'FETCH_ERROR'
167
+ */
168
+ type ErrorsOfDeps<Deps extends Record<string, AnyResultFn>> = ErrorOf<Deps[keyof Deps]>;
169
+ /**
170
+ * Extract union of cause types from a deps object.
171
+ * Example: CausesOfDeps<{ fetchUser: typeof fetchUser }> where fetchUser returns Result<User, "NOT_FOUND", Error>
172
+ * yields: Error
173
+ *
174
+ * Note: This represents the domain cause types from declared functions.
175
+ * However, workflow results may also have unknown causes from step.try failures
176
+ * or uncaught exceptions, so the actual Result cause type is `unknown`.
177
+ */
178
+ type CausesOfDeps<Deps extends Record<string, AnyResultFn>> = CauseOf<Deps[keyof Deps]>;
179
+ /**
180
+ * Non-strict workflow options
181
+ * Returns E | UnexpectedError (safe default)
182
+ */
183
+ type WorkflowOptions<E, C = void> = {
184
+ onError?: (error: E | UnexpectedError, stepName?: string) => void;
185
+ /** Unified event stream for workflow and step lifecycle */
186
+ onEvent?: (event: WorkflowEvent<E | UnexpectedError>, ctx: C) => void;
187
+ /** Create per-run context for event correlation */
188
+ createContext?: () => C;
189
+ /** Step result cache - only steps with a `key` option are cached */
190
+ cache?: StepCache;
191
+ /** Pre-populate cache from saved state for workflow resume */
192
+ resumeState?: ResumeState | (() => ResumeState | Promise<ResumeState>);
193
+ catchUnexpected?: never;
194
+ strict?: false;
195
+ };
196
+ /**
197
+ * Strict workflow options
198
+ * Returns E | U (closed error union)
199
+ */
200
+ type WorkflowOptionsStrict<E, U, C = void> = {
201
+ strict: true;
202
+ catchUnexpected: (cause: unknown) => U;
203
+ onError?: (error: E | U, stepName?: string) => void;
204
+ /** Unified event stream for workflow and step lifecycle */
205
+ onEvent?: (event: WorkflowEvent<E | U>, ctx: C) => void;
206
+ /** Create per-run context for event correlation */
207
+ createContext?: () => C;
208
+ /** Step result cache - only steps with a `key` option are cached */
209
+ cache?: StepCache;
210
+ /** Pre-populate cache from saved state for workflow resume */
211
+ resumeState?: ResumeState | (() => ResumeState | Promise<ResumeState>);
212
+ };
213
+ /**
214
+ * Workflow return type (non-strict)
215
+ * Supports both argument-less and argument-passing call patterns
216
+ *
217
+ * Note: Cause type is `unknown` because:
218
+ * - step.try errors have thrown values as cause
219
+ * - Uncaught exceptions produce unknown causes
220
+ * - Different steps may have different cause types
221
+ * The cause IS preserved at runtime; narrow based on error type if needed.
222
+ */
223
+ interface Workflow<E, Deps> {
224
+ /**
225
+ * Execute workflow without arguments (original API)
226
+ */
227
+ <T>(fn: (step: RunStep<E>, deps: Deps) => T | Promise<T>): AsyncResult<T, E | UnexpectedError, unknown>;
228
+ /**
229
+ * Execute workflow with typed arguments
230
+ * @param args - Typed arguments passed to the callback (type inferred at call site)
231
+ * @param fn - Callback receives (step, deps, args)
232
+ */
233
+ <T, Args>(args: Args, fn: (step: RunStep<E>, deps: Deps, args: Args) => T | Promise<T>): AsyncResult<T, E | UnexpectedError, unknown>;
234
+ }
235
+ /**
236
+ * Workflow return type (strict)
237
+ * Supports both argument-less and argument-passing call patterns
238
+ *
239
+ * Note: Cause type is `unknown` because catchUnexpected receives thrown
240
+ * values which have unknown type.
241
+ */
242
+ interface WorkflowStrict<E, U, Deps> {
243
+ /**
244
+ * Execute workflow without arguments (original API)
245
+ */
246
+ <T>(fn: (step: RunStep<E>, deps: Deps) => T | Promise<T>): AsyncResult<T, E | U, unknown>;
247
+ /**
248
+ * Execute workflow with typed arguments
249
+ * @param args - Typed arguments passed to the callback (type inferred at call site)
250
+ * @param fn - Callback receives (step, deps, args)
251
+ */
252
+ <T, Args>(args: Args, fn: (step: RunStep<E>, deps: Deps, args: Args) => T | Promise<T>): AsyncResult<T, E | U, unknown>;
253
+ }
254
+ /**
255
+ * Create a typed workflow with automatic error inference.
256
+ *
257
+ * ## When to Use `createWorkflow`
258
+ *
259
+ * Use `createWorkflow` when you have:
260
+ * - **Multiple dependent async operations** that need to run sequentially
261
+ * - **Complex error handling** where you want type-safe error unions
262
+ * - **Need for observability** via event streams (onEvent)
263
+ * - **Step caching** requirements for expensive operations
264
+ * - **Resume/replay** capabilities for long-running workflows
265
+ * - **Human-in-the-loop** workflows requiring approvals
266
+ *
267
+ * ## Why Use `createWorkflow` Instead of `run()`
268
+ *
269
+ * 1. **Automatic Error Type Inference**: Errors are computed from your declared functions
270
+ * - No manual error union management
271
+ * - TypeScript ensures all possible errors are handled
272
+ * - Refactoring is safer - adding/removing functions updates error types automatically
273
+ *
274
+ * 2. **Step Caching**: Expensive operations can be cached by key
275
+ * - Prevents duplicate API calls
276
+ * - Useful for idempotent operations
277
+ * - Supports resume state for workflow replay
278
+ *
279
+ * 3. **Event Stream**: Built-in observability via `onEvent`
280
+ * - Track workflow and step lifecycle
281
+ * - Monitor performance (durationMs)
282
+ * - Build dashboards and debugging tools
283
+ *
284
+ * 4. **Resume State**: Save and replay workflows
285
+ * - Useful for long-running processes
286
+ * - Supports human-in-the-loop workflows
287
+ * - Enables workflow persistence across restarts
288
+ *
289
+ * ## How It Works
290
+ *
291
+ * 1. **Declare Dependencies**: Pass an object of Result-returning functions
292
+ * 2. **Automatic Inference**: Error types are extracted from function return types
293
+ * 3. **Execute Workflow**: Call the returned workflow function with your logic
294
+ * 4. **Early Exit**: `step()` unwraps Results - on error, workflow exits immediately
295
+ *
296
+ * ## Error Type Inference
297
+ *
298
+ * The error union is automatically computed from all declared functions:
299
+ * - Each function's error type is extracted
300
+ * - Union of all errors is created
301
+ * - `UnexpectedError` is added for uncaught exceptions (unless strict mode)
302
+ *
303
+ * ## Strict Mode
304
+ *
305
+ * Use `strict: true` with `catchUnexpected` for closed error unions:
306
+ * - Removes `UnexpectedError` from the union
307
+ * - All errors must be explicitly handled
308
+ * - Useful for production code where you want exhaustive error handling
309
+ *
310
+ * @param deps - Object mapping names to Result-returning functions.
311
+ * These functions must return `Result<T, E>` or `Promise<Result<T, E>>`.
312
+ * The error types (`E`) from all functions are automatically combined into a union.
313
+ * @param options - Optional configuration:
314
+ * - `onEvent`: Callback for workflow/step lifecycle events
315
+ * - `onError`: Callback for error logging/debugging
316
+ * - `cache`: Step result cache (Map or custom StepCache implementation)
317
+ * - `resumeState`: Pre-populated step results for workflow replay
318
+ * - `createContext`: Factory for per-run context (passed to onEvent)
319
+ * - `strict`: Enable strict mode (requires `catchUnexpected`)
320
+ * - `catchUnexpected`: Map uncaught exceptions to typed errors (required in strict mode)
321
+ *
322
+ * @returns A workflow function that accepts your workflow logic and returns an AsyncResult.
323
+ * The error type is automatically inferred from the `deps` parameter.
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * // Basic usage - automatic error inference
328
+ * const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> =>
329
+ * id === '1' ? ok({ id, name: 'Alice' }) : err('NOT_FOUND');
330
+ *
331
+ * const fetchPosts = async (userId: string): AsyncResult<Post[], 'FETCH_ERROR'> =>
332
+ * ok([{ id: 1, title: 'Hello' }]);
333
+ *
334
+ * const getPosts = createWorkflow({ fetchUser, fetchPosts });
335
+ *
336
+ * const result = await getPosts(async (step) => {
337
+ * const user = await step(fetchUser('1'));
338
+ * const posts = await step(fetchPosts(user.id));
339
+ * return { user, posts };
340
+ * });
341
+ * // result.error: 'NOT_FOUND' | 'FETCH_ERROR' | UnexpectedError
342
+ * ```
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * // With destructuring in callback (optional but convenient)
347
+ * const result = await getPosts(async (step, { fetchUser, fetchPosts }) => {
348
+ * const user = await step(fetchUser('1'));
349
+ * const posts = await step(fetchPosts(user.id));
350
+ * return { user, posts };
351
+ * });
352
+ * ```
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * // Strict mode - closed error union (no UnexpectedError)
357
+ * const getPosts = createWorkflow(
358
+ * { fetchUser, fetchPosts },
359
+ * {
360
+ * strict: true,
361
+ * catchUnexpected: () => 'UNEXPECTED' as const
362
+ * }
363
+ * );
364
+ * // result.error: 'NOT_FOUND' | 'FETCH_ERROR' | 'UNEXPECTED' (exactly)
365
+ * ```
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * // With step caching
370
+ * const cache = new Map<string, Result<unknown, unknown>>();
371
+ * const workflow = createWorkflow({ fetchUser }, { cache });
372
+ *
373
+ * const result = await workflow(async (step) => {
374
+ * // First call executes fetchUser
375
+ * const user1 = await step(() => fetchUser('1'), { key: 'user:1' });
376
+ * // Second call with same key uses cache (fetchUser not called again)
377
+ * const user2 = await step(() => fetchUser('1'), { key: 'user:1' });
378
+ * return user1; // user1 === user2
379
+ * });
380
+ * ```
381
+ *
382
+ * @example
383
+ * ```typescript
384
+ * // With event stream for observability
385
+ * const workflow = createWorkflow({ fetchUser }, {
386
+ * onEvent: (event) => {
387
+ * if (event.type === 'step_start') {
388
+ * console.log(`Step ${event.name} started`);
389
+ * }
390
+ * if (event.type === 'step_success') {
391
+ * console.log(`Step ${event.name} completed in ${event.durationMs}ms`);
392
+ * }
393
+ * }
394
+ * });
395
+ * ```
396
+ *
397
+ * @example
398
+ * ```typescript
399
+ * // With resume state for workflow replay
400
+ * const savedState = { steps: new Map([['user:1', { result: ok({ id: '1', name: 'Alice' }) }]]) };
401
+ * const workflow = createWorkflow({ fetchUser }, { resumeState: savedState });
402
+ *
403
+ * const result = await workflow(async (step) => {
404
+ * // This step uses cached result from savedState (fetchUser not called)
405
+ * const user = await step(() => fetchUser('1'), { key: 'user:1' });
406
+ * return user;
407
+ * });
408
+ * ```
409
+ *
410
+ * @example
411
+ * ```typescript
412
+ * // With typed arguments (new API)
413
+ * const workflow = createWorkflow({ fetchUser, fetchPosts });
414
+ *
415
+ * const result = await workflow(
416
+ * { userId: '1' }, // Typed arguments
417
+ * async (step, { fetchUser, fetchPosts }, { userId }) => {
418
+ * const user = await step(fetchUser(userId));
419
+ * const posts = await step(fetchPosts(user.id));
420
+ * return { user, posts };
421
+ * }
422
+ * );
423
+ * ```
424
+ */
425
+ declare function createWorkflow<const Deps extends Readonly<Record<string, AnyResultFn>>, C = void>(deps: Deps, options?: WorkflowOptions<ErrorsOfDeps<Deps>, C>): Workflow<ErrorsOfDeps<Deps>, Deps>;
426
+ declare function createWorkflow<const Deps extends Readonly<Record<string, AnyResultFn>>, U, C = void>(deps: Deps, options: WorkflowOptionsStrict<ErrorsOfDeps<Deps>, U, C>): WorkflowStrict<ErrorsOfDeps<Deps>, U, Deps>;
427
+ /**
428
+ * Type guard to check if an event is a step_complete event.
429
+ * Use this to filter events for state persistence.
430
+ *
431
+ * @param event - The workflow event to check
432
+ * @returns `true` if the event is a step_complete event, `false` otherwise
433
+ *
434
+ * @example
435
+ * ```typescript
436
+ * const savedSteps = new Map<string, Result<unknown, unknown>>();
437
+ *
438
+ * const workflow = createWorkflow({ fetchUser }, {
439
+ * onEvent: (event) => {
440
+ * if (isStepComplete(event)) {
441
+ * savedSteps.set(event.stepKey, event.result);
442
+ * }
443
+ * }
444
+ * });
445
+ * ```
446
+ */
447
+ declare function isStepComplete(event: WorkflowEvent<unknown>): event is Extract<WorkflowEvent<unknown>, {
448
+ type: "step_complete";
449
+ }>;
450
+ /**
451
+ * Standard error type for steps awaiting human approval.
452
+ * Use this as the error type for approval-gated steps.
453
+ *
454
+ * @example
455
+ * const requireApproval = async (userId: string): AsyncResult<Approval, PendingApproval> => {
456
+ * const status = await checkApprovalStatus(userId);
457
+ * if (status === 'pending') {
458
+ * return err({ type: 'PENDING_APPROVAL', stepKey: `approval:${userId}` });
459
+ * }
460
+ * return ok(status.approval);
461
+ * };
462
+ */
463
+ type PendingApproval = {
464
+ type: "PENDING_APPROVAL";
465
+ /** Step key for correlation when resuming */
466
+ stepKey: string;
467
+ /** Optional reason for the pending state */
468
+ reason?: string;
469
+ /** Optional metadata for the approval request */
470
+ metadata?: Record<string, unknown>;
471
+ };
472
+ /**
473
+ * Error returned when approval is rejected.
474
+ */
475
+ type ApprovalRejected = {
476
+ type: "APPROVAL_REJECTED";
477
+ /** Step key for correlation */
478
+ stepKey: string;
479
+ /** Reason the approval was rejected */
480
+ reason: string;
481
+ };
482
+ /**
483
+ * Type guard to check if an error is a PendingApproval.
484
+ *
485
+ * @param error - The error to check
486
+ * @returns `true` if the error is a PendingApproval, `false` otherwise
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * const result = await workflow(...);
491
+ * if (!result.ok && isPendingApproval(result.error)) {
492
+ * console.log(`Waiting for approval: ${result.error.stepKey}`);
493
+ * }
494
+ * ```
495
+ */
496
+ declare function isPendingApproval(error: unknown): error is PendingApproval;
497
+ /**
498
+ * Type guard to check if an error is an ApprovalRejected.
499
+ *
500
+ * @param error - The error to check
501
+ * @returns `true` if the error is an ApprovalRejected, `false` otherwise
502
+ */
503
+ declare function isApprovalRejected(error: unknown): error is ApprovalRejected;
504
+ /**
505
+ * Create a PendingApproval error result.
506
+ * Convenience helper for approval-gated steps.
507
+ *
508
+ * @param stepKey - Stable key for this approval step (used for resume)
509
+ * @param options - Optional reason and metadata for the pending approval
510
+ * @returns A Result with a PendingApproval error
511
+ *
512
+ * @example
513
+ * ```typescript
514
+ * const requireApproval = async (userId: string) => {
515
+ * const status = await db.getApproval(userId);
516
+ * if (!status) return pendingApproval(`approval:${userId}`);
517
+ * return ok(status);
518
+ * };
519
+ * ```
520
+ */
521
+ declare function pendingApproval(stepKey: string, options?: {
522
+ reason?: string;
523
+ metadata?: Record<string, unknown>;
524
+ }): Result<never, PendingApproval>;
525
+ /**
526
+ * Options for creating an approval-gated step.
527
+ */
528
+ interface ApprovalStepOptions<T> {
529
+ /** Stable key for this approval step (used for resume) */
530
+ key: string;
531
+ /** Function to check current approval status from external source */
532
+ checkApproval: () => Promise<{
533
+ status: "pending";
534
+ } | {
535
+ status: "approved";
536
+ value: T;
537
+ } | {
538
+ status: "rejected";
539
+ reason: string;
540
+ }>;
541
+ /** Optional reason shown when pending */
542
+ pendingReason?: string;
543
+ /** Optional metadata for the approval request */
544
+ metadata?: Record<string, unknown>;
545
+ }
546
+ /**
547
+ * Create a Result-returning function that checks external approval status.
548
+ *
549
+ * ## When to Use
550
+ *
551
+ * Use `createApprovalStep` when you need:
552
+ * - **Human-in-the-loop workflows**: Steps that require human approval
553
+ * - **External approval systems**: Integrate with approval databases/APIs
554
+ * - **Workflow pausing**: Workflows that pause and resume after approval
555
+ * - **Approval tracking**: Track who approved what and when
556
+ *
557
+ * ## Why Use This Instead of Manual Approval Checks
558
+ *
559
+ * - **Standardized pattern**: Consistent approval step interface
560
+ * - **Type-safe**: Returns typed `PendingApproval` or `ApprovalRejected` errors
561
+ * - **Resume-friendly**: Works seamlessly with `injectApproval()` and resume state
562
+ * - **Metadata support**: Can include approval reason and metadata
563
+ *
564
+ * ## How It Works
565
+ *
566
+ * 1. Create approval step with `checkApproval` function
567
+ * 2. `checkApproval` returns one of:
568
+ * - `{ status: 'pending' }` - Approval not yet granted (workflow pauses)
569
+ * - `{ status: 'approved', value: T }` - Approval granted (workflow continues)
570
+ * - `{ status: 'rejected', reason: string }` - Approval rejected (workflow fails)
571
+ * 3. Use in workflow with `step()` - workflow pauses if pending
572
+ * 4. When approval granted externally, use `injectApproval()` to resume
573
+ *
574
+ * ## Typical Approval Flow
575
+ *
576
+ * 1. Workflow executes → reaches approval step
577
+ * 2. `checkApproval()` called → returns `{ status: 'pending' }`
578
+ * 3. Workflow returns `PendingApproval` error
579
+ * 4. Save workflow state → persist for later resume
580
+ * 5. Show approval UI → user sees pending approval
581
+ * 6. User grants/rejects → update approval system
582
+ * 7. Inject approval → call `injectApproval()` with approved value
583
+ * 8. Resume workflow → continue from approval step
584
+ *
585
+ * @param options - Configuration for the approval step:
586
+ * - `key`: Stable key for this approval (must match step key in workflow)
587
+ * - `checkApproval`: Async function that checks current approval status
588
+ * - `pendingReason`: Optional reason shown when approval is pending
589
+ * - `metadata`: Optional metadata attached to the approval request
590
+ *
591
+ * @returns A function that returns an AsyncResult checking approval status.
592
+ * The function can be used directly with `step()` in workflows.
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * // Create approval step that checks database
597
+ * const requireManagerApproval = createApprovalStep<{ approvedBy: string }>({
598
+ * key: 'manager-approval',
599
+ * checkApproval: async () => {
600
+ * const approval = await db.getApproval('manager-approval');
601
+ * if (!approval) {
602
+ * return { status: 'pending' }; // Workflow pauses here
603
+ * }
604
+ * if (approval.rejected) {
605
+ * return { status: 'rejected', reason: approval.reason };
606
+ * }
607
+ * return {
608
+ * status: 'approved',
609
+ * value: { approvedBy: approval.approvedBy }
610
+ * };
611
+ * },
612
+ * pendingReason: 'Waiting for manager approval',
613
+ * });
614
+ *
615
+ * // Use in workflow
616
+ * const workflow = createWorkflow({ requireManagerApproval });
617
+ * const result = await workflow(async (step) => {
618
+ * const approval = await step(requireManagerApproval, { key: 'manager-approval' });
619
+ * // If pending, workflow exits with PendingApproval error
620
+ * // If approved, continues with approval value
621
+ * return approval;
622
+ * });
623
+ *
624
+ * // Handle pending state
625
+ * if (!result.ok && isPendingApproval(result.error)) {
626
+ * // Workflow paused - show approval UI
627
+ * showApprovalUI(result.error.stepKey);
628
+ * }
629
+ * ```
630
+ *
631
+ * @example
632
+ * ```typescript
633
+ * // With approval injection for resume
634
+ * const collector = createHITLCollector();
635
+ * const workflow = createWorkflow({ requireApproval }, {
636
+ * onEvent: collector.handleEvent,
637
+ * });
638
+ *
639
+ * const result = await workflow(async (step) => {
640
+ * const approval = await step(requireApproval, { key: 'approval:1' });
641
+ * return approval;
642
+ * });
643
+ *
644
+ * // When approval granted externally
645
+ * if (collector.hasPendingApprovals()) {
646
+ * const resumeState = collector.injectApproval('approval:1', {
647
+ * approvedBy: 'admin@example.com'
648
+ * });
649
+ *
650
+ * // Resume workflow
651
+ * const workflow2 = createWorkflow({ requireApproval }, { resumeState });
652
+ * const result2 = await workflow2(async (step) => {
653
+ * const approval = await step(requireApproval, { key: 'approval:1' });
654
+ * return approval; // Now succeeds with injected value
655
+ * });
656
+ * }
657
+ * ```
658
+ */
659
+ declare function createApprovalStep<T>(options: ApprovalStepOptions<T>): () => AsyncResult<T, PendingApproval | ApprovalRejected>;
660
+ /**
661
+ * Inject an approved value into resume state.
662
+ * Use this when an external approval is granted and you want to resume the workflow.
663
+ *
664
+ * @param state - The resume state to update
665
+ * @param options - Object with stepKey and the approved value
666
+ * @returns A new ResumeState with the approval injected
667
+ *
668
+ * @example
669
+ * ```typescript
670
+ * // When approval is granted externally:
671
+ * const updatedState = injectApproval(savedState, {
672
+ * stepKey: 'deploy:prod',
673
+ * value: { approvedBy: 'admin', approvedAt: Date.now() }
674
+ * });
675
+ *
676
+ * // Resume workflow with the approval injected
677
+ * const workflow = createWorkflow({ ... }, { resumeState: updatedState });
678
+ * ```
679
+ */
680
+ declare function injectApproval<T>(state: ResumeState, options: {
681
+ stepKey: string;
682
+ value: T;
683
+ }): ResumeState;
684
+ /**
685
+ * Remove a step from resume state (e.g., to force re-execution).
686
+ *
687
+ * @param state - The resume state to update
688
+ * @param stepKey - The key of the step to remove
689
+ * @returns A new ResumeState with the step removed
690
+ *
691
+ * @example
692
+ * ```typescript
693
+ * // Force a step to re-execute on resume
694
+ * const updatedState = clearStep(savedState, 'approval:123');
695
+ * ```
696
+ */
697
+ declare function clearStep(state: ResumeState, stepKey: string): ResumeState;
698
+ /**
699
+ * Check if a step in resume state has a pending approval error.
700
+ *
701
+ * @param state - The resume state to check
702
+ * @param stepKey - The key of the step to check
703
+ * @returns `true` if the step has a pending approval, `false` otherwise
704
+ *
705
+ * @example
706
+ * ```typescript
707
+ * if (hasPendingApproval(savedState, 'deploy:prod')) {
708
+ * // Show approval UI
709
+ * }
710
+ * ```
711
+ */
712
+ declare function hasPendingApproval(state: ResumeState, stepKey: string): boolean;
713
+ /**
714
+ * Get all pending approval step keys from resume state.
715
+ *
716
+ * @param state - The resume state to check
717
+ * @returns Array of step keys that have pending approvals
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * const pendingKeys = getPendingApprovals(savedState);
722
+ * // ['deploy:prod', 'deploy:staging']
723
+ * ```
724
+ */
725
+ declare function getPendingApprovals(state: ResumeState): string[];
726
+ /**
727
+ * Extended step collector that tracks pending approvals.
728
+ * Use this for HITL workflows that need to track approval state.
729
+ *
730
+ * @returns An object with methods to handle events, get state, and manage approvals
731
+ *
732
+ * @example
733
+ * ```typescript
734
+ * const collector = createHITLCollector();
735
+ *
736
+ * const workflow = createWorkflow({ fetchUser, requireApproval }, {
737
+ * onEvent: collector.handleEvent,
738
+ * });
739
+ *
740
+ * const result = await workflow(async (step) => {
741
+ * const user = await step(() => fetchUser("1"), { key: "user:1" });
742
+ * const approval = await step(requireApproval, { key: "approval:1" });
743
+ * return { user, approval };
744
+ * });
745
+ *
746
+ * // Check for pending approvals
747
+ * if (collector.hasPendingApprovals()) {
748
+ * const pending = collector.getPendingApprovals();
749
+ * // pending: [{ stepKey: 'approval:1', error: PendingApproval }]
750
+ * await saveToDatabase(collector.getState());
751
+ * }
752
+ *
753
+ * // Later, when approved:
754
+ * const resumeState = collector.injectApproval('approval:1', { approvedBy: 'admin' });
755
+ * ```
756
+ */
757
+ declare function createHITLCollector(): {
758
+ /** Handle workflow events (pass to onEvent option) */
759
+ handleEvent: (event: WorkflowEvent<unknown>) => void;
760
+ /** Get collected resume state */
761
+ getState: () => ResumeState;
762
+ /** Clear all collected state */
763
+ clear: () => void;
764
+ /** Check if any steps have pending approvals */
765
+ hasPendingApprovals: () => boolean;
766
+ /** Get all pending approval entries with their errors */
767
+ getPendingApprovals: () => Array<{
768
+ stepKey: string;
769
+ error: PendingApproval;
770
+ }>;
771
+ /** Inject an approval result, updating the collector's internal state. Returns a copy for use as resumeState. */
772
+ injectApproval: <T>(stepKey: string, value: T) => ResumeState;
773
+ };
774
+
775
+ export { type AnyResultFn, type ApprovalRejected, type ApprovalStepOptions, AsyncResult, type CausesOfDeps, type ErrorsOfDeps, type PendingApproval, Result, type ResumeState, type ResumeStateEntry, RunStep, type StepCache, UnexpectedError, type Workflow, WorkflowEvent, type WorkflowOptions, type WorkflowOptionsStrict, type WorkflowStrict, clearStep, createApprovalStep, createHITLCollector, createStepCollector, createWorkflow, getPendingApprovals, hasPendingApproval, injectApproval, isApprovalRejected, isPendingApproval, isStepComplete, pendingApproval };