@multiplier-labs/stepflow 0.3.4 → 0.3.8

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 (109) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-5XJBAPXF.js → chunk-4IDPWWGS.js} +15 -4
  3. package/dist/chunk-4IDPWWGS.js.map +1 -0
  4. package/dist/{chunk-U3PCVK6O.js → chunk-T6GAZBTQ.js} +70 -38
  5. package/dist/chunk-T6GAZBTQ.js.map +1 -0
  6. package/dist/{chunk-FMPJBK2N.js → chunk-TVSADUYU.js} +3 -5
  7. package/dist/{chunk-FMPJBK2N.js.map → chunk-TVSADUYU.js.map} +1 -1
  8. package/dist/events/index.d.ts +43 -3
  9. package/dist/events/index.js +7 -3
  10. package/dist/{index-Cd0IZ40L.d.ts → index-CdXDv30T.d.ts} +10 -2
  11. package/dist/index.d.ts +5 -4
  12. package/dist/index.js +159 -46
  13. package/dist/index.js.map +1 -1
  14. package/dist/scheduler/index.d.ts +2 -2
  15. package/dist/scheduler/index.js +1 -1
  16. package/dist/storage/index.d.ts +6 -4
  17. package/dist/storage/index.js +1 -1
  18. package/dist/{types-DSEjiLMj.d.ts → types-BfWe5yP6.d.ts} +2 -0
  19. package/package.json +26 -14
  20. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  21. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  22. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  23. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  24. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  25. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  26. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  27. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  28. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  29. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  30. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  31. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  32. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  33. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  34. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  35. package/.github/CODEOWNERS +0 -15
  36. package/.github/dependabot.yml +0 -23
  37. package/.github/workflows/ci.yml +0 -38
  38. package/.github/workflows/publish.yml +0 -42
  39. package/.github/workflows/security.yml +0 -29
  40. package/CHANGELOG.md +0 -34
  41. package/SECURITY.md +0 -40
  42. package/dist/chunk-5XJBAPXF.js.map +0 -1
  43. package/dist/chunk-U3PCVK6O.js.map +0 -1
  44. package/docs/github-packages-setup.md +0 -97
  45. package/docs/stepflow-api-reference.md +0 -1663
  46. package/src/core/engine/concurrency.ts +0 -229
  47. package/src/core/engine/lifecycle.ts +0 -176
  48. package/src/core/engine.test.ts +0 -1435
  49. package/src/core/engine.ts +0 -598
  50. package/src/core/index.ts +0 -7
  51. package/src/core/orchestrator/persistence.ts +0 -165
  52. package/src/core/orchestrator/plan-resolution.ts +0 -140
  53. package/src/core/orchestrator/state-transitions.ts +0 -109
  54. package/src/core/orchestrator/step-runner.ts +0 -427
  55. package/src/core/orchestrator.ts +0 -252
  56. package/src/core/types.ts +0 -220
  57. package/src/events/index.ts +0 -8
  58. package/src/events/memory.test.ts +0 -107
  59. package/src/events/memory.ts +0 -92
  60. package/src/events/socketio.test.ts +0 -378
  61. package/src/events/socketio.ts +0 -262
  62. package/src/events/types.ts +0 -118
  63. package/src/events/webhook.test.ts +0 -949
  64. package/src/events/webhook.ts +0 -807
  65. package/src/index.ts +0 -270
  66. package/src/planning/evaluate-condition.test.ts +0 -153
  67. package/src/planning/evaluate-condition.ts +0 -105
  68. package/src/planning/index.ts +0 -55
  69. package/src/planning/planner.ts +0 -494
  70. package/src/planning/planning.test.ts +0 -727
  71. package/src/planning/registry.test.ts +0 -398
  72. package/src/planning/registry.ts +0 -283
  73. package/src/planning/types.ts +0 -566
  74. package/src/scheduler/cron.test.ts +0 -479
  75. package/src/scheduler/cron.ts +0 -438
  76. package/src/scheduler/index.ts +0 -8
  77. package/src/scheduler/postgres-persistence.ts +0 -530
  78. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  79. package/src/scheduler/sqlite-persistence.ts +0 -286
  80. package/src/scheduler/types.ts +0 -73
  81. package/src/storage/index.ts +0 -27
  82. package/src/storage/memory.test.ts +0 -349
  83. package/src/storage/memory.ts +0 -227
  84. package/src/storage/postgres-core.ts +0 -550
  85. package/src/storage/postgres-migration.test.ts +0 -69
  86. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  87. package/src/storage/postgres.ts +0 -835
  88. package/src/storage/sqlite-core.ts +0 -151
  89. package/src/storage/sqlite.test.ts +0 -642
  90. package/src/storage/sqlite.ts +0 -717
  91. package/src/storage/types.ts +0 -343
  92. package/src/utils/errors.test.ts +0 -150
  93. package/src/utils/errors.ts +0 -161
  94. package/src/utils/id.ts +0 -21
  95. package/src/utils/index.ts +0 -10
  96. package/src/utils/logger.test.ts +0 -121
  97. package/src/utils/logger.ts +0 -96
  98. package/src/utils/pinned-fetch.test.ts +0 -84
  99. package/src/utils/pinned-fetch.ts +0 -118
  100. package/src/utils/postgres-deps.ts +0 -79
  101. package/src/utils/retry.test.ts +0 -177
  102. package/src/utils/retry.ts +0 -121
  103. package/src/utils/safe-json.test.ts +0 -166
  104. package/src/utils/safe-json.ts +0 -107
  105. package/src/utils/webhook-signature.test.ts +0 -111
  106. package/src/utils/webhook-signature.ts +0 -74
  107. package/tsconfig.json +0 -21
  108. package/tsup.config.ts +0 -14
  109. package/vitest.config.ts +0 -9
@@ -1,427 +0,0 @@
1
- /**
2
- * Step runner — executes a single workflow step with hooks, retries,
3
- * cancellation, and timeout handling.
4
- *
5
- * The shape of the previous monolithic `executeStep` was a `while (true)`
6
- * loop containing a `try`/`catch` with multiple nested `if` branches per
7
- * error path (cancel, hook, skip, retry, fail). This file flattens that:
8
- *
9
- * - Each error path is its own intent-named helper
10
- * (handleSkipAction, handleRetryAction, runOnStepErrorHook, etc.)
11
- * - The main loop nesting is at most 3 levels (while → try → switch)
12
- *
13
- * State-transition rules covered here (see also state-transitions.ts):
14
- *
15
- * pre-attempt:
16
- * - abort signal already raised → throw WorkflowCanceledError
17
- *
18
- * per-attempt:
19
- * - handler resolves → markStepSucceeded, emit step.completed, return
20
- * - handler rejects + canceled → markStepFailed(canceled), emit step.failed, rethrow
21
- * - handler rejects + policy=skip → emit step.skipped, return undefined
22
- * - handler rejects + policy=retry → emit step.retry, sleep, continue
23
- * - handler rejects + policy=fail → throw StepError
24
- */
25
- import type {
26
- WorkflowDefinition,
27
- WorkflowStep,
28
- WorkflowContext,
29
- Logger,
30
- } from '../types';
31
- import type { StorageAdapter, WorkflowRunStepRecord } from '../../storage/types';
32
- import type { EventTransport } from '../../events/types';
33
- import {
34
- StepError,
35
- StepTimeoutError,
36
- WorkflowCanceledError,
37
- } from '../../utils/errors';
38
- import { sleep, calculateRetryDelay } from '../../utils/retry';
39
- import {
40
- createStepRecord,
41
- markStepSucceeded,
42
- markStepFailed,
43
- markStepCanceled,
44
- safeEmitEvent,
45
- } from './persistence';
46
- import {
47
- resolveStepErrorPolicy,
48
- decideStepFailureAction,
49
- type StepErrorPolicy,
50
- } from './state-transitions';
51
-
52
- /**
53
- * Inputs to `executeStep` beyond the step + context themselves.
54
- */
55
- export interface StepExecutionOptions<TInput = Record<string, unknown>> {
56
- definition: WorkflowDefinition<TInput>;
57
- storage: StorageAdapter;
58
- events: EventTransport;
59
- logger: Logger;
60
- abortController: AbortController;
61
- }
62
-
63
- /**
64
- * Execute a single workflow step with error handling and retries.
65
- *
66
- * Returns the step's result on success, or `undefined` if the resolved
67
- * error policy is `skip` and the step failed. Throws on cancellation,
68
- * timeout, or `fail` policy.
69
- */
70
- export async function executeStep<TInput>(
71
- step: WorkflowStep<TInput>,
72
- context: WorkflowContext<TInput>,
73
- options: StepExecutionOptions<TInput>
74
- ): Promise<unknown> {
75
- const { definition, storage, events, logger, abortController } = options;
76
- const policy = resolveStepErrorPolicy(step, definition);
77
-
78
- let attempt = 0;
79
-
80
- while (true) {
81
- attempt++;
82
- throwIfCanceled(abortController, context.runId);
83
-
84
- const stepRecord = await beginStepAttempt(
85
- storage,
86
- events,
87
- logger,
88
- context,
89
- step,
90
- attempt
91
- );
92
-
93
- if (definition.hooks?.beforeStep) {
94
- await definition.hooks.beforeStep(context, step);
95
- }
96
-
97
- try {
98
- const result = await runStepHandler(step, context, abortController);
99
- await completeStepAttempt(
100
- storage,
101
- events,
102
- logger,
103
- stepRecord,
104
- context,
105
- step,
106
- result,
107
- definition
108
- );
109
- return result;
110
- } catch (error) {
111
- const lastError = toError(error);
112
- const isCanceled =
113
- error instanceof WorkflowCanceledError || abortController.signal.aborted;
114
-
115
- await markStepFailed(storage, stepRecord.id, error, isCanceled);
116
-
117
- if (isCanceled) {
118
- emitStepFailedEvent(events, logger, context, step, lastError, attempt);
119
- throw error;
120
- }
121
-
122
- await runOnStepErrorHook(definition, context, step, lastError, logger);
123
- emitStepFailedEvent(events, logger, context, step, lastError, attempt);
124
-
125
- const action = decideStepFailureAction(policy, attempt);
126
-
127
- if (action === 'skip') {
128
- handleSkipAction(events, logger, context, step, lastError);
129
- return undefined;
130
- }
131
-
132
- if (action === 'retry') {
133
- await handleRetryAction(
134
- storage,
135
- events,
136
- logger,
137
- abortController,
138
- context,
139
- step,
140
- attempt,
141
- policy,
142
- lastError,
143
- stepRecord.id
144
- );
145
- continue;
146
- }
147
-
148
- // action === 'fail' — explicit fail strategy or retries exhausted
149
- throw new StepError(step.key, lastError.message, attempt, lastError);
150
- }
151
- }
152
- }
153
-
154
- /**
155
- * Throw a WorkflowCanceledError if the abort signal has already been raised.
156
- * Called at the top of each attempt so we don't even create a step record
157
- * after cancellation.
158
- */
159
- function throwIfCanceled(abortController: AbortController, runId: string): void {
160
- if (abortController.signal.aborted) {
161
- throw new WorkflowCanceledError(runId);
162
- }
163
- }
164
-
165
- /**
166
- * Persist a fresh step record for this attempt, set it as the current step
167
- * id on the context (so handlers can attach granular telemetry), and emit
168
- * `step.started`.
169
- */
170
- async function beginStepAttempt<TInput>(
171
- storage: StorageAdapter,
172
- events: EventTransport,
173
- logger: Logger,
174
- context: WorkflowContext<TInput>,
175
- step: WorkflowStep<TInput>,
176
- attempt: number
177
- ): Promise<WorkflowRunStepRecord> {
178
- const stepRecord = await createStepRecord(
179
- storage,
180
- context.runId,
181
- step.key,
182
- step.name,
183
- attempt
184
- );
185
- context.stepId = stepRecord.id;
186
- safeEmitEvent(events, logger, {
187
- runId: context.runId,
188
- kind: context.kind,
189
- eventType: 'step.started',
190
- stepKey: step.key,
191
- timestamp: new Date(),
192
- payload: { attempt },
193
- });
194
- return stepRecord;
195
- }
196
-
197
- /**
198
- * Invoke the step handler. If the step has its own timeout, race against it
199
- * AND the abort signal; otherwise just race against the abort signal so a
200
- * workflow-level timeout/cancellation still interrupts the step.
201
- */
202
- async function runStepHandler<TInput>(
203
- step: WorkflowStep<TInput>,
204
- context: WorkflowContext<TInput>,
205
- abortController: AbortController
206
- ): Promise<unknown> {
207
- if (step.timeout) {
208
- return executeWithTimeout(
209
- () => step.handler(context),
210
- step.timeout,
211
- abortController.signal,
212
- step.key
213
- );
214
- }
215
- return raceWithAbort(step.handler(context), abortController.signal);
216
- }
217
-
218
- /**
219
- * Mark the step as succeeded, run the afterStep hook (if any), and emit
220
- * `step.completed`.
221
- */
222
- async function completeStepAttempt<TInput>(
223
- storage: StorageAdapter,
224
- events: EventTransport,
225
- logger: Logger,
226
- stepRecord: WorkflowRunStepRecord,
227
- context: WorkflowContext<TInput>,
228
- step: WorkflowStep<TInput>,
229
- result: unknown,
230
- definition: WorkflowDefinition<TInput>
231
- ): Promise<void> {
232
- await markStepSucceeded(storage, stepRecord.id, result);
233
- if (definition.hooks?.afterStep) {
234
- await definition.hooks.afterStep(context, step, result);
235
- }
236
- safeEmitEvent(events, logger, {
237
- runId: context.runId,
238
- kind: context.kind,
239
- eventType: 'step.completed',
240
- stepKey: step.key,
241
- timestamp: new Date(),
242
- payload: { result },
243
- });
244
- }
245
-
246
- /**
247
- * Coerce an unknown thrown value into a real Error, preserving message
248
- * text for non-Error throws.
249
- */
250
- function toError(error: unknown): Error {
251
- return error instanceof Error ? error : new Error(String(error));
252
- }
253
-
254
- /**
255
- * Run the user's onStepError hook (if defined). Hook failures are logged
256
- * but never propagated — they must not mask the original step error.
257
- */
258
- async function runOnStepErrorHook<TInput>(
259
- definition: WorkflowDefinition<TInput>,
260
- context: WorkflowContext<TInput>,
261
- step: WorkflowStep<TInput>,
262
- lastError: Error,
263
- logger: Logger
264
- ): Promise<void> {
265
- if (!definition.hooks?.onStepError) return;
266
- try {
267
- await definition.hooks.onStepError(context, step, lastError);
268
- } catch (hookError) {
269
- logger.error('onStepError hook failed for run ' + context.runId + ':', hookError);
270
- }
271
- }
272
-
273
- /**
274
- * Emit a `step.failed` event with the canonical payload.
275
- */
276
- function emitStepFailedEvent<TInput>(
277
- events: EventTransport,
278
- logger: Logger,
279
- context: WorkflowContext<TInput>,
280
- step: WorkflowStep<TInput>,
281
- lastError: Error,
282
- attempt: number
283
- ): void {
284
- safeEmitEvent(events, logger, {
285
- runId: context.runId,
286
- kind: context.kind,
287
- eventType: 'step.failed',
288
- stepKey: step.key,
289
- timestamp: new Date(),
290
- payload: { error: lastError.message, attempt },
291
- });
292
- }
293
-
294
- /**
295
- * Apply the `skip` failure action: warn, emit `step.skipped`, then the
296
- * caller returns `undefined` from the step.
297
- */
298
- function handleSkipAction<TInput>(
299
- events: EventTransport,
300
- logger: Logger,
301
- context: WorkflowContext<TInput>,
302
- step: WorkflowStep<TInput>,
303
- lastError: Error
304
- ): void {
305
- logger.warn(`Step "${step.key}" failed, skipping (strategy: skip)`);
306
- safeEmitEvent(events, logger, {
307
- runId: context.runId,
308
- kind: context.kind,
309
- eventType: 'step.skipped',
310
- stepKey: step.key,
311
- timestamp: new Date(),
312
- payload: { reason: 'error', error: lastError.message },
313
- });
314
- }
315
-
316
- /**
317
- * Apply the `retry` failure action: emit `step.retry`, sleep with backoff,
318
- * and let the loop fall through to the next attempt. If the sleep itself
319
- * is interrupted by cancellation, mark the step record canceled before
320
- * rethrowing.
321
- */
322
- async function handleRetryAction<TInput>(
323
- storage: StorageAdapter,
324
- events: EventTransport,
325
- logger: Logger,
326
- abortController: AbortController,
327
- context: WorkflowContext<TInput>,
328
- step: WorkflowStep<TInput>,
329
- attempt: number,
330
- policy: StepErrorPolicy,
331
- lastError: Error,
332
- stepRecordId: string
333
- ): Promise<void> {
334
- const delay = calculateRetryDelay(attempt, policy.retryDelay, policy.retryBackoff);
335
- logger.warn(
336
- `Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${policy.maxRetries})`
337
- );
338
- safeEmitEvent(events, logger, {
339
- runId: context.runId,
340
- kind: context.kind,
341
- eventType: 'step.retry',
342
- stepKey: step.key,
343
- timestamp: new Date(),
344
- payload: { attempt, maxRetries: policy.maxRetries, delay, error: lastError.message },
345
- });
346
-
347
- try {
348
- await sleep(delay, abortController.signal);
349
- } catch (sleepError) {
350
- if (sleepError instanceof WorkflowCanceledError) {
351
- await markStepCanceled(storage, stepRecordId);
352
- }
353
- throw sleepError;
354
- }
355
- }
356
-
357
- /**
358
- * Execute a function with a timeout.
359
- *
360
- * Uses Promise.race to resolve as soon as either the function completes, the
361
- * timeout fires, or the abort signal triggers. The `finally` block is critical:
362
- * without it, the losing promise's timer/listener would remain active, leaking
363
- * memory — especially problematic in long-running engines processing many steps.
364
- */
365
- export async function executeWithTimeout<T>(
366
- fn: () => Promise<T>,
367
- timeoutMs: number,
368
- signal: AbortSignal,
369
- stepKey: string
370
- ): Promise<T> {
371
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
372
- let onAbort: (() => void) | undefined;
373
-
374
- try {
375
- return await Promise.race([
376
- fn(),
377
- new Promise<never>((_, reject) => {
378
- timeoutId = setTimeout(() => {
379
- reject(new StepTimeoutError(stepKey, timeoutMs));
380
- }, timeoutMs);
381
-
382
- onAbort = () => {
383
- clearTimeout(timeoutId);
384
- reject(new WorkflowCanceledError('run'));
385
- };
386
- signal.addEventListener('abort', onAbort, { once: true });
387
- }),
388
- ]);
389
- } finally {
390
- if (timeoutId) clearTimeout(timeoutId);
391
- if (onAbort) signal.removeEventListener('abort', onAbort);
392
- }
393
- }
394
-
395
- /**
396
- * Race a promise against an abort signal.
397
- *
398
- * Steps without their own timeout still need to respect workflow-level timeouts
399
- * and cancellation. This wraps the step handler in a Promise.race so that an
400
- * abort signal (from workflow timeout or user cancellation) can interrupt it.
401
- *
402
- * The `finally` cleanup removes the abort listener to prevent accumulating
403
- * listeners on the signal across many steps — without it, each step would
404
- * leave a dangling listener even after completing successfully.
405
- */
406
- export async function raceWithAbort<T>(
407
- promise: Promise<T>,
408
- signal: AbortSignal
409
- ): Promise<T> {
410
- if (signal.aborted) {
411
- throw new WorkflowCanceledError('run');
412
- }
413
-
414
- let onAbort: (() => void) | undefined;
415
-
416
- try {
417
- return await Promise.race([
418
- promise,
419
- new Promise<never>((_, reject) => {
420
- onAbort = () => reject(new WorkflowCanceledError('run'));
421
- signal.addEventListener('abort', onAbort, { once: true });
422
- }),
423
- ]);
424
- } finally {
425
- if (onAbort) signal.removeEventListener('abort', onAbort);
426
- }
427
- }
@@ -1,252 +0,0 @@
1
- /**
2
- * Workflow Orchestrator
3
- *
4
- * This module is the heart of the workflow execution system.
5
- * It manages the execution of workflow steps, handles errors,
6
- * emits events, and persists state.
7
- */
8
-
9
- import type {
10
- WorkflowDefinition,
11
- RunResult,
12
- Logger,
13
- SpawnChildOptions,
14
- } from './types';
15
- import type { StorageAdapter } from '../storage/types';
16
- import type { EventTransport } from '../events/types';
17
- import { WorkflowEngineError, WorkflowCanceledError } from '../utils/errors';
18
- import {
19
- markRunRunning,
20
- saveCheckpoint,
21
- markRunSucceeded,
22
- markRunFinal,
23
- safeEmitEvent,
24
- } from './orchestrator/persistence';
25
- import { decideRunFinalState } from './orchestrator/state-transitions';
26
- import { executeStep } from './orchestrator/step-runner';
27
- import {
28
- buildContext,
29
- setupWorkflowTimeout,
30
- initialCompletedSteps,
31
- } from './orchestrator/plan-resolution';
32
-
33
- /**
34
- * Checkpoint data for resuming workflows.
35
- */
36
- export interface WorkflowCheckpoint {
37
- /** Set of step keys that have been completed */
38
- completedStepKeys: Set<string>;
39
- /** Accumulated results from completed steps */
40
- results: Record<string, unknown>;
41
- }
42
-
43
- /**
44
- * Options for executing a workflow.
45
- */
46
- export interface ExecuteOptions {
47
- /** The run ID */
48
- runId: string;
49
- /** The workflow definition */
50
- definition: WorkflowDefinition;
51
- /** Input parameters */
52
- input: Record<string, unknown>;
53
- /** Optional metadata */
54
- metadata?: Record<string, unknown>;
55
- /** Storage adapter */
56
- storage: StorageAdapter;
57
- /** Event transport */
58
- events: EventTransport;
59
- /** Logger instance */
60
- logger: Logger;
61
- /** Abort controller for cancellation */
62
- abortController: AbortController;
63
- /** Function to spawn child workflows */
64
- spawnChild: (options: SpawnChildOptions) => Promise<string>;
65
- /** Optional checkpoint for resuming from a previous execution */
66
- checkpoint?: WorkflowCheckpoint;
67
- }
68
-
69
- /**
70
- * Execute a workflow definition.
71
- * This is the main execution logic that runs all steps sequentially.
72
- */
73
- export async function executeWorkflow(options: ExecuteOptions): Promise<RunResult> {
74
- const {
75
- runId,
76
- definition,
77
- input,
78
- metadata = {},
79
- storage,
80
- events,
81
- logger,
82
- abortController,
83
- spawnChild,
84
- checkpoint,
85
- } = options;
86
-
87
- const startTime = Date.now();
88
- const isResume = !!checkpoint;
89
-
90
- // Update run status to running (if not already)
91
- // On resume, don't overwrite the original startedAt
92
- await markRunRunning(storage, runId, isResume);
93
-
94
- safeEmitEvent(events, logger, {
95
- runId,
96
- kind: definition.kind,
97
- eventType: isResume ? 'run.resumed' : 'run.started',
98
- timestamp: new Date(),
99
- payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : undefined,
100
- });
101
-
102
- const completedSteps = initialCompletedSteps(checkpoint);
103
-
104
- const context = buildContext({
105
- runId,
106
- definition,
107
- input,
108
- metadata,
109
- events,
110
- logger,
111
- abortController,
112
- spawnChild,
113
- checkpointResults: checkpoint?.results,
114
- });
115
-
116
- const timeoutHandle = setupWorkflowTimeout(runId, definition, abortController);
117
-
118
- try {
119
- // Execute beforeRun hook
120
- if (definition.hooks?.beforeRun) {
121
- await definition.hooks.beforeRun(context);
122
- }
123
-
124
- // Execute each step in sequence
125
- for (const step of definition.steps) {
126
- // Check for cancellation
127
- if (abortController.signal.aborted) {
128
- throw new WorkflowCanceledError(runId);
129
- }
130
-
131
- context.currentStep = step.key;
132
-
133
- // Skip already completed steps when resuming
134
- if (checkpoint?.completedStepKeys.has(step.key)) {
135
- logger.info(`Skipping step "${step.key}" (already completed in previous run)`);
136
- safeEmitEvent(events, logger, {
137
- runId,
138
- kind: definition.kind,
139
- eventType: 'step.skipped',
140
- stepKey: step.key,
141
- timestamp: new Date(),
142
- payload: { reason: 'checkpoint' },
143
- });
144
- continue;
145
- }
146
-
147
- // Check skip condition
148
- if (step.skipIf) {
149
- const shouldSkip = await step.skipIf(context);
150
- if (shouldSkip) {
151
- logger.info(`Skipping step "${step.key}" (condition met)`);
152
-
153
- safeEmitEvent(events, logger, {
154
- runId,
155
- kind: definition.kind,
156
- eventType: 'step.skipped',
157
- stepKey: step.key,
158
- timestamp: new Date(),
159
- });
160
-
161
- continue;
162
- }
163
- }
164
-
165
- // Execute the step
166
- const result = await executeStep(step, context, {
167
- definition,
168
- storage,
169
- events,
170
- logger,
171
- abortController,
172
- });
173
-
174
- // Store result in context
175
- context.results[step.key] = result;
176
-
177
- // Track completed step
178
- completedSteps.push(step.key);
179
-
180
- // Save checkpoint (accumulated results + completed steps)
181
- await saveCheckpoint(storage, runId, context.results, completedSteps);
182
- }
183
-
184
- timeoutHandle.clear();
185
-
186
- const duration = Date.now() - startTime;
187
- const runResult: RunResult = {
188
- status: 'succeeded',
189
- results: context.results,
190
- duration,
191
- };
192
-
193
- // Execute afterRun hook
194
- if (definition.hooks?.afterRun) {
195
- await definition.hooks.afterRun(context, runResult);
196
- }
197
-
198
- // Update run status
199
- await markRunSucceeded(storage, runId, context.results);
200
-
201
- safeEmitEvent(events, logger, {
202
- runId,
203
- kind: definition.kind,
204
- eventType: 'run.completed',
205
- timestamp: new Date(),
206
- payload: { results: context.results, duration },
207
- });
208
-
209
- return runResult;
210
-
211
- } catch (error) {
212
- timeoutHandle.clear();
213
-
214
- const duration = Date.now() - startTime;
215
-
216
- const actualError = timeoutHandle.getTimeoutError() ?? error;
217
- const workflowError = WorkflowEngineError.fromError(actualError);
218
-
219
- // Determine final status + event type from the surfaced error.
220
- const { status, eventType } = decideRunFinalState(actualError);
221
-
222
- const runResult: RunResult = {
223
- status,
224
- results: context.results,
225
- error: workflowError,
226
- duration,
227
- };
228
-
229
- // Execute afterRun hook (even on failure)
230
- if (definition.hooks?.afterRun) {
231
- try {
232
- await definition.hooks.afterRun(context, runResult);
233
- } catch (hookError) {
234
- logger.error('afterRun hook failed for run ' + runId + ':', hookError);
235
- }
236
- }
237
-
238
- // Update run status
239
- await markRunFinal(storage, runId, status, context.results, workflowError);
240
-
241
- safeEmitEvent(events, logger, {
242
- runId,
243
- kind: definition.kind,
244
- eventType,
245
- timestamp: new Date(),
246
- payload: { error: workflowError.message, duration },
247
- });
248
-
249
- return runResult;
250
- }
251
- }
252
-