@marktoflow/core 2.0.0-alpha.15 → 2.0.0-alpha.16

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 +24 -365
  2. package/dist/built-in-operations.d.ts +10 -0
  3. package/dist/built-in-operations.d.ts.map +1 -1
  4. package/dist/built-in-operations.js +386 -1
  5. package/dist/built-in-operations.js.map +1 -1
  6. package/dist/credentials.d.ts +60 -1
  7. package/dist/credentials.d.ts.map +1 -1
  8. package/dist/credentials.js +229 -4
  9. package/dist/credentials.js.map +1 -1
  10. package/dist/engine.d.ts +36 -1
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +462 -21
  13. package/dist/engine.js.map +1 -1
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +9 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/models.d.ts +399 -6
  19. package/dist/models.d.ts.map +1 -1
  20. package/dist/models.js +60 -1
  21. package/dist/models.js.map +1 -1
  22. package/dist/oauth-manager.d.ts +128 -0
  23. package/dist/oauth-manager.d.ts.map +1 -0
  24. package/dist/oauth-manager.js +291 -0
  25. package/dist/oauth-manager.js.map +1 -0
  26. package/dist/oauth-refresh.d.ts +37 -0
  27. package/dist/oauth-refresh.d.ts.map +1 -0
  28. package/dist/oauth-refresh.js +76 -0
  29. package/dist/oauth-refresh.js.map +1 -0
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +19 -0
  32. package/dist/parser.js.map +1 -1
  33. package/dist/sdk-registry.d.ts +7 -1
  34. package/dist/sdk-registry.d.ts.map +1 -1
  35. package/dist/sdk-registry.js +38 -9
  36. package/dist/sdk-registry.js.map +1 -1
  37. package/dist/secret-providers/index.d.ts +12 -0
  38. package/dist/secret-providers/index.d.ts.map +1 -0
  39. package/dist/secret-providers/index.js +11 -0
  40. package/dist/secret-providers/index.js.map +1 -0
  41. package/dist/secret-providers/providers/aws.d.ts +32 -0
  42. package/dist/secret-providers/providers/aws.d.ts.map +1 -0
  43. package/dist/secret-providers/providers/aws.js +118 -0
  44. package/dist/secret-providers/providers/aws.js.map +1 -0
  45. package/dist/secret-providers/providers/azure.d.ts +40 -0
  46. package/dist/secret-providers/providers/azure.d.ts.map +1 -0
  47. package/dist/secret-providers/providers/azure.js +170 -0
  48. package/dist/secret-providers/providers/azure.js.map +1 -0
  49. package/dist/secret-providers/providers/env.d.ts +26 -0
  50. package/dist/secret-providers/providers/env.d.ts.map +1 -0
  51. package/dist/secret-providers/providers/env.js +59 -0
  52. package/dist/secret-providers/providers/env.js.map +1 -0
  53. package/dist/secret-providers/providers/vault.d.ts +39 -0
  54. package/dist/secret-providers/providers/vault.d.ts.map +1 -0
  55. package/dist/secret-providers/providers/vault.js +180 -0
  56. package/dist/secret-providers/providers/vault.js.map +1 -0
  57. package/dist/secret-providers/secret-manager.d.ts +72 -0
  58. package/dist/secret-providers/secret-manager.d.ts.map +1 -0
  59. package/dist/secret-providers/secret-manager.js +226 -0
  60. package/dist/secret-providers/secret-manager.js.map +1 -0
  61. package/dist/secret-providers/types.d.ts +105 -0
  62. package/dist/secret-providers/types.d.ts.map +1 -0
  63. package/dist/secret-providers/types.js +8 -0
  64. package/dist/secret-providers/types.js.map +1 -0
  65. package/dist/secrets/index.d.ts +12 -0
  66. package/dist/secrets/index.d.ts.map +1 -0
  67. package/dist/secrets/index.js +11 -0
  68. package/dist/secrets/index.js.map +1 -0
  69. package/dist/secrets/providers/aws.d.ts +32 -0
  70. package/dist/secrets/providers/aws.d.ts.map +1 -0
  71. package/dist/secrets/providers/aws.js +118 -0
  72. package/dist/secrets/providers/aws.js.map +1 -0
  73. package/dist/secrets/providers/azure.d.ts +40 -0
  74. package/dist/secrets/providers/azure.d.ts.map +1 -0
  75. package/dist/secrets/providers/azure.js +170 -0
  76. package/dist/secrets/providers/azure.js.map +1 -0
  77. package/dist/secrets/providers/env.d.ts +26 -0
  78. package/dist/secrets/providers/env.d.ts.map +1 -0
  79. package/dist/secrets/providers/env.js +59 -0
  80. package/dist/secrets/providers/env.js.map +1 -0
  81. package/dist/secrets/providers/vault.d.ts +39 -0
  82. package/dist/secrets/providers/vault.d.ts.map +1 -0
  83. package/dist/secrets/providers/vault.js +180 -0
  84. package/dist/secrets/providers/vault.js.map +1 -0
  85. package/dist/secrets/secret-manager.d.ts +72 -0
  86. package/dist/secrets/secret-manager.d.ts.map +1 -0
  87. package/dist/secrets/secret-manager.js +226 -0
  88. package/dist/secrets/secret-manager.js.map +1 -0
  89. package/dist/secrets/types.d.ts +105 -0
  90. package/dist/secrets/types.d.ts.map +1 -0
  91. package/dist/secrets/types.js +8 -0
  92. package/dist/secrets/types.js.map +1 -0
  93. package/package.json +1 -1
  94. package/dist/expression-helpers.d.ts +0 -309
  95. package/dist/expression-helpers.d.ts.map +0 -1
  96. package/dist/expression-helpers.js +0 -697
  97. package/dist/expression-helpers.js.map +0 -1
  98. package/dist/pipeline-parser.d.ts +0 -38
  99. package/dist/pipeline-parser.d.ts.map +0 -1
  100. package/dist/pipeline-parser.js +0 -219
  101. package/dist/pipeline-parser.js.map +0 -1
  102. package/dist/regex-operators.d.ts +0 -86
  103. package/dist/regex-operators.d.ts.map +0 -1
  104. package/dist/regex-operators.js +0 -383
  105. package/dist/regex-operators.js.map +0 -1
  106. package/dist/version.d.ts +0 -8
  107. package/dist/version.d.ts.map +0 -1
  108. package/dist/version.js +0 -8
  109. package/dist/version.js.map +0 -1
package/dist/engine.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Executes workflow steps with retry logic, variable resolution,
5
5
  * and SDK invocation.
6
6
  */
7
- import { StepStatus, WorkflowStatus, createExecutionContext, createStepResult, isActionStep, isSubWorkflowStep, isIfStep, isSwitchStep, isForEachStep, isWhileStep, isMapStep, isFilterStep, isReduceStep, isParallelStep, isTryStep, isScriptStep, } from './models.js';
7
+ import { StepStatus, WorkflowStatus, createExecutionContext, createStepResult, isActionStep, isSubWorkflowStep, isIfStep, isSwitchStep, isForEachStep, isWhileStep, isMapStep, isFilterStep, isReduceStep, isParallelStep, isTryStep, isScriptStep, isWaitStep, isMergeStep, } from './models.js';
8
8
  import { mergePermissions, toSecurityPolicy, } from './permissions.js';
9
9
  import { loadPromptFile, resolvePromptTemplate, validatePromptInputs, } from './prompt-loader.js';
10
10
  import { DEFAULT_FAILOVER_CONFIG, AgentHealthTracker, FailoverReason, } from './failover.js';
@@ -16,6 +16,28 @@ import { executeScriptAsync } from './script-executor.js';
16
16
  // ============================================================================
17
17
  // Helper Functions
18
18
  // ============================================================================
19
+ /**
20
+ * Parse a duration string like "2h", "30m", "5s", "100ms" into milliseconds.
21
+ */
22
+ function parseDuration(duration) {
23
+ const match = duration.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i);
24
+ if (!match) {
25
+ const asNum = Number(duration);
26
+ if (!isNaN(asNum))
27
+ return asNum; // Treat bare numbers as milliseconds
28
+ throw new Error(`Invalid duration format: "${duration}". Use format like "2h", "30m", "5s", "100ms"`);
29
+ }
30
+ const value = parseFloat(match[1]);
31
+ const unit = match[2].toLowerCase();
32
+ switch (unit) {
33
+ case 'ms': return value;
34
+ case 's': return value * 1000;
35
+ case 'm': return value * 60 * 1000;
36
+ case 'h': return value * 60 * 60 * 1000;
37
+ case 'd': return value * 24 * 60 * 60 * 1000;
38
+ default: return value;
39
+ }
40
+ }
19
41
  /**
20
42
  * Convert error to string for display/logging
21
43
  */
@@ -33,6 +55,21 @@ function errorToString(error) {
33
55
  return String(error);
34
56
  }
35
57
  }
58
+ /**
59
+ * Get a field value from an object by key path.
60
+ */
61
+ function getField(item, field) {
62
+ if (!item || typeof item !== 'object')
63
+ return undefined;
64
+ const parts = field.split('.');
65
+ let current = item;
66
+ for (const part of parts) {
67
+ if (!current || typeof current !== 'object')
68
+ return undefined;
69
+ current = current[part];
70
+ }
71
+ return current;
72
+ }
36
73
  // ============================================================================
37
74
  // Retry Policy
38
75
  // ============================================================================
@@ -247,7 +284,7 @@ export class WorkflowEngine {
247
284
  failoverConfig;
248
285
  healthTracker;
249
286
  failoverEvents = [];
250
- workflowPath; // Base path for resolving sub-workflows
287
+ workflowPath; // Base path for resolving sub-workflows (public for CLI state tracking)
251
288
  workflowPermissions; // Workflow-level permissions
252
289
  promptCache = new Map(); // Cache for loaded prompts
253
290
  constructor(config = {}, events = {}, stateStore) {
@@ -305,6 +342,12 @@ export class WorkflowEngine {
305
342
  if (isScriptStep(step)) {
306
343
  return this.executeScriptStep(step, context);
307
344
  }
345
+ if (isWaitStep(step)) {
346
+ return this.executeWaitStep(step, context);
347
+ }
348
+ if (isMergeStep(step)) {
349
+ return this.executeMergeStep(step, context);
350
+ }
308
351
  // Default: action or workflow step
309
352
  return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
310
353
  }
@@ -330,7 +373,7 @@ export class WorkflowEngine {
330
373
  this.stateStore.createExecution({
331
374
  runId: context.runId,
332
375
  workflowId: workflow.metadata.id,
333
- workflowPath: 'unknown',
376
+ workflowPath: this.workflowPath ?? 'unknown',
334
377
  status: WorkflowStatus.RUNNING,
335
378
  startedAt: startedAt,
336
379
  completedAt: null,
@@ -427,6 +470,136 @@ export class WorkflowEngine {
427
470
  this.events.onWorkflowComplete?.(workflow, workflowResult);
428
471
  return workflowResult;
429
472
  }
473
+ /**
474
+ * Resume a paused execution (e.g., after form submission).
475
+ *
476
+ * @param runId - The execution run ID
477
+ * @param stepId - The step ID that was waiting
478
+ * @param resumeData - Data from the resume action (e.g., form submission)
479
+ * @param sdkRegistry - SDK registry for step execution
480
+ * @param stepExecutor - Step executor function
481
+ * @returns Workflow result from resumed execution
482
+ */
483
+ async resumeExecution(runId, stepId, resumeData, sdkRegistry, stepExecutor) {
484
+ if (!this.stateStore) {
485
+ throw new Error('Cannot resume execution: StateStore not configured');
486
+ }
487
+ // Load execution from state store
488
+ const execution = this.stateStore.getExecution(runId);
489
+ if (!execution) {
490
+ throw new Error(`Execution ${runId} not found`);
491
+ }
492
+ if (execution.status !== WorkflowStatus.RUNNING) {
493
+ throw new Error(`Cannot resume execution ${runId}: status is ${execution.status}`);
494
+ }
495
+ // Load workflow
496
+ const { workflow } = await parseFile(execution.workflowPath);
497
+ this.workflowPath = execution.workflowPath;
498
+ // Find the step that was waiting
499
+ const stepIndex = workflow.steps.findIndex(s => s.id === stepId);
500
+ if (stepIndex === -1) {
501
+ throw new Error(`Step ${stepId} not found in workflow`);
502
+ }
503
+ // Load all checkpoints to rebuild context
504
+ const checkpoints = this.stateStore.getCheckpoints(runId);
505
+ const stepResults = [];
506
+ const context = createExecutionContext(workflow, execution.inputs || {});
507
+ context.runId = runId;
508
+ context.status = WorkflowStatus.RUNNING;
509
+ // Rebuild context from checkpoints
510
+ for (let i = 0; i < stepIndex; i++) {
511
+ const checkpoint = checkpoints.find(cp => cp.stepIndex === i);
512
+ if (checkpoint) {
513
+ const step = workflow.steps[i];
514
+ // Recreate step result
515
+ const result = createStepResult(step.id, checkpoint.status, checkpoint.outputs, checkpoint.startedAt, checkpoint.retryCount, checkpoint.error || undefined);
516
+ stepResults.push(result);
517
+ // Restore step metadata
518
+ context.stepMetadata[step.id] = {
519
+ status: checkpoint.status.toLowerCase(),
520
+ retryCount: checkpoint.retryCount,
521
+ ...(checkpoint.error ? { error: checkpoint.error } : {}),
522
+ };
523
+ // Restore output variable
524
+ if (step.outputVariable && checkpoint.status === StepStatus.COMPLETED) {
525
+ context.variables[step.outputVariable] = checkpoint.outputs;
526
+ }
527
+ }
528
+ }
529
+ // Inject resume data into context
530
+ context.variables[`${stepId}_response`] = resumeData;
531
+ // Continue execution from next step
532
+ const startedAt = new Date(execution.startedAt);
533
+ this.workflowPermissions = workflow.permissions;
534
+ try {
535
+ for (let i = stepIndex + 1; i < workflow.steps.length; i++) {
536
+ const step = workflow.steps[i];
537
+ context.currentStepIndex = i;
538
+ const result = await this.executeStep(step, context, sdkRegistry, stepExecutor);
539
+ stepResults.push(result);
540
+ context.stepMetadata[step.id] = {
541
+ status: result.status.toLowerCase(),
542
+ retryCount: result.retryCount,
543
+ ...(result.error ? { error: errorToString(result.error) } : {}),
544
+ };
545
+ if (step.outputVariable && result.status === StepStatus.COMPLETED) {
546
+ context.variables[step.outputVariable] = result.output;
547
+ }
548
+ if (result.status === StepStatus.COMPLETED &&
549
+ result.output &&
550
+ typeof result.output === 'object' &&
551
+ '__workflow_outputs__' in result.output) {
552
+ const outputObj = result.output;
553
+ const outputs = outputObj['__workflow_outputs__'];
554
+ context.workflowOutputs = outputs;
555
+ }
556
+ if (result.status === StepStatus.FAILED) {
557
+ let errorAction = 'stop';
558
+ if ('errorHandling' in step && step.errorHandling?.action) {
559
+ errorAction = step.errorHandling.action;
560
+ }
561
+ if (errorAction === 'stop' || errorAction === 'rollback') {
562
+ if (errorAction === 'rollback' && this.rollbackRegistry) {
563
+ await this.rollbackRegistry.rollbackAllAsync({
564
+ context,
565
+ inputs: context.inputs,
566
+ variables: context.variables,
567
+ });
568
+ }
569
+ context.status = WorkflowStatus.FAILED;
570
+ const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
571
+ const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
572
+ this.events.onWorkflowComplete?.(workflow, workflowResult);
573
+ return workflowResult;
574
+ }
575
+ }
576
+ }
577
+ context.status = WorkflowStatus.COMPLETED;
578
+ }
579
+ catch (error) {
580
+ context.status = WorkflowStatus.FAILED;
581
+ if (this.stateStore) {
582
+ this.stateStore.updateExecution(runId, {
583
+ status: WorkflowStatus.FAILED,
584
+ completedAt: new Date(),
585
+ error: error instanceof Error ? error.message : String(error),
586
+ });
587
+ }
588
+ const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, error instanceof Error ? error.message : String(error));
589
+ this.events.onWorkflowComplete?.(workflow, workflowResult);
590
+ return workflowResult;
591
+ }
592
+ const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt);
593
+ if (this.stateStore) {
594
+ this.stateStore.updateExecution(runId, {
595
+ status: context.status,
596
+ completedAt: new Date(),
597
+ outputs: context.variables,
598
+ });
599
+ }
600
+ this.events.onWorkflowComplete?.(workflow, workflowResult);
601
+ return workflowResult;
602
+ }
430
603
  /**
431
604
  * Execute a workflow from a file.
432
605
  * This method automatically sets the workflow path for resolving sub-workflows.
@@ -1116,9 +1289,17 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
1116
1289
  }
1117
1290
  /**
1118
1291
  * Execute a for-each loop step.
1292
+ * Supports optional batch_size and pause_between_batches for rate limiting.
1119
1293
  */
1120
1294
  async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
1121
1295
  const startedAt = new Date();
1296
+ const cleanupLoopVars = () => {
1297
+ delete context.variables[step.itemVariable];
1298
+ delete context.variables['loop'];
1299
+ delete context.variables['batch'];
1300
+ if (step.indexVariable)
1301
+ delete context.variables[step.indexVariable];
1302
+ };
1122
1303
  try {
1123
1304
  // Resolve items array
1124
1305
  const items = resolveTemplates(step.items, context);
@@ -1128,10 +1309,15 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
1128
1309
  if (items.length === 0) {
1129
1310
  return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
1130
1311
  }
1131
- // Execute steps for each item
1312
+ const batchSize = step.batchSize;
1313
+ const pauseBetweenBatches = step.pauseBetweenBatches;
1314
+ // If batch mode, process items in batches
1315
+ if (batchSize && batchSize > 0) {
1316
+ return await this.executeForEachBatched(step, items, batchSize, pauseBetweenBatches ?? 0, context, sdkRegistry, stepExecutor, startedAt);
1317
+ }
1318
+ // Standard item-by-item execution
1132
1319
  const results = [];
1133
1320
  for (let i = 0; i < items.length; i++) {
1134
- // Inject loop variables
1135
1321
  context.variables[step.itemVariable] = items[i];
1136
1322
  context.variables['loop'] = {
1137
1323
  index: i,
@@ -1151,35 +1337,82 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
1151
1337
  if (result.status === StepStatus.FAILED) {
1152
1338
  const errorAction = step.errorHandling?.action ?? 'stop';
1153
1339
  if (errorAction === 'stop') {
1154
- // Clean up loop variables
1155
- delete context.variables[step.itemVariable];
1156
- delete context.variables['loop'];
1157
- if (step.indexVariable)
1158
- delete context.variables[step.indexVariable];
1340
+ cleanupLoopVars();
1159
1341
  return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
1160
1342
  }
1161
- // 'continue' - skip to next iteration
1162
1343
  break;
1163
1344
  }
1164
1345
  }
1165
1346
  results.push(context.variables[step.itemVariable]);
1166
1347
  }
1167
- // Clean up loop variables
1168
- delete context.variables[step.itemVariable];
1169
- delete context.variables['loop'];
1170
- if (step.indexVariable)
1171
- delete context.variables[step.indexVariable];
1348
+ cleanupLoopVars();
1172
1349
  return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
1173
1350
  }
1174
1351
  catch (error) {
1175
- // Clean up loop variables on error
1176
- delete context.variables[step.itemVariable];
1177
- delete context.variables['loop'];
1178
- if (step.indexVariable)
1179
- delete context.variables[step.indexVariable];
1352
+ cleanupLoopVars();
1180
1353
  return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1181
1354
  }
1182
1355
  }
1356
+ /**
1357
+ * Execute for-each in batch mode.
1358
+ * Items are split into batches; {{ batch }} contains the current batch array.
1359
+ */
1360
+ async executeForEachBatched(step, items, batchSize, pauseBetweenBatches, context, sdkRegistry, stepExecutor, startedAt) {
1361
+ const results = [];
1362
+ const totalBatches = Math.ceil(items.length / batchSize);
1363
+ for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
1364
+ const batchStart = batchIndex * batchSize;
1365
+ const batchItems = items.slice(batchStart, batchStart + batchSize);
1366
+ // Pause between batches (not before first batch)
1367
+ if (batchIndex > 0 && pauseBetweenBatches > 0) {
1368
+ await new Promise((resolve) => setTimeout(resolve, pauseBetweenBatches));
1369
+ }
1370
+ // Expose batch-level variables
1371
+ context.variables['batch'] = batchItems;
1372
+ context.variables['loop'] = {
1373
+ index: batchIndex,
1374
+ first: batchIndex === 0,
1375
+ last: batchIndex === totalBatches - 1,
1376
+ length: totalBatches,
1377
+ batchSize,
1378
+ batchStart,
1379
+ totalItems: items.length,
1380
+ };
1381
+ // Process each item in the batch
1382
+ for (let i = 0; i < batchItems.length; i++) {
1383
+ const globalIndex = batchStart + i;
1384
+ context.variables[step.itemVariable] = batchItems[i];
1385
+ if (step.indexVariable) {
1386
+ context.variables[step.indexVariable] = globalIndex;
1387
+ }
1388
+ for (const iterStep of step.steps) {
1389
+ const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
1390
+ if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
1391
+ context.variables[iterStep.outputVariable] = result.output;
1392
+ }
1393
+ if (result.status === StepStatus.FAILED) {
1394
+ const errorAction = step.errorHandling?.action ?? 'stop';
1395
+ if (errorAction === 'stop') {
1396
+ delete context.variables[step.itemVariable];
1397
+ delete context.variables['loop'];
1398
+ delete context.variables['batch'];
1399
+ if (step.indexVariable)
1400
+ delete context.variables[step.indexVariable];
1401
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
1402
+ }
1403
+ break;
1404
+ }
1405
+ }
1406
+ results.push(context.variables[step.itemVariable]);
1407
+ }
1408
+ }
1409
+ delete context.variables[step.itemVariable];
1410
+ delete context.variables['loop'];
1411
+ delete context.variables['batch'];
1412
+ if (step.indexVariable)
1413
+ delete context.variables[step.indexVariable];
1414
+ return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
1415
+ }
1183
1416
  /**
1184
1417
  * Execute a while loop step.
1185
1418
  */
@@ -1436,6 +1669,214 @@ Execute the workflow steps in order and return the final outputs as JSON.`;
1436
1669
  }
1437
1670
  }
1438
1671
  // ============================================================================
1672
+ // Wait Step Execution
1673
+ // ============================================================================
1674
+ /**
1675
+ * Execute a wait/pause step.
1676
+ *
1677
+ * For mode=duration: In-process wait (suitable for short durations).
1678
+ * For persistent long-duration waits, a WaitManager should checkpoint
1679
+ * the execution and resume it later via the scheduler.
1680
+ *
1681
+ * For mode=webhook: Returns a resume URL. The execution will be
1682
+ * checkpointed and resumed when the URL is called.
1683
+ *
1684
+ * For mode=form: Returns form fields. The execution will be
1685
+ * checkpointed and resumed when the form is submitted.
1686
+ */
1687
+ async executeWaitStep(step, context) {
1688
+ const startedAt = new Date();
1689
+ try {
1690
+ switch (step.mode) {
1691
+ case 'duration': {
1692
+ if (!step.duration) {
1693
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Wait step with mode=duration requires a duration');
1694
+ }
1695
+ const resolvedDuration = resolveTemplates(step.duration, context);
1696
+ const ms = parseDuration(resolvedDuration);
1697
+ // For short durations (under 5 minutes), do in-process wait
1698
+ if (ms <= 300000) {
1699
+ await new Promise((resolve) => setTimeout(resolve, ms));
1700
+ return createStepResult(step.id, StepStatus.COMPLETED, { waited: ms }, startedAt);
1701
+ }
1702
+ // For longer durations, checkpoint and schedule resume
1703
+ // The StateStore will persist the execution state
1704
+ if (this.stateStore) {
1705
+ this.stateStore.saveCheckpoint({
1706
+ runId: context.runId,
1707
+ stepIndex: context.currentStepIndex,
1708
+ stepName: step.id,
1709
+ status: StepStatus.COMPLETED,
1710
+ startedAt: startedAt,
1711
+ completedAt: new Date(),
1712
+ inputs: { mode: 'duration', resumeAt: new Date(Date.now() + ms).toISOString() },
1713
+ outputs: { waiting: true },
1714
+ error: null,
1715
+ retryCount: 0,
1716
+ });
1717
+ }
1718
+ // Set execution status to indicate waiting
1719
+ const resumeAt = new Date(Date.now() + ms).toISOString();
1720
+ return createStepResult(step.id, StepStatus.COMPLETED, {
1721
+ waiting: true,
1722
+ mode: 'duration',
1723
+ resumeAt,
1724
+ durationMs: ms,
1725
+ }, startedAt);
1726
+ }
1727
+ case 'webhook': {
1728
+ // Generate a unique resume URL
1729
+ const resumeToken = crypto.randomUUID();
1730
+ const webhookPath = step.webhookPath
1731
+ ? resolveTemplates(step.webhookPath, context)
1732
+ : `/resume/${context.runId}/${step.id}/${resumeToken}`;
1733
+ if (this.stateStore) {
1734
+ this.stateStore.saveCheckpoint({
1735
+ runId: context.runId,
1736
+ stepIndex: context.currentStepIndex,
1737
+ stepName: step.id,
1738
+ status: StepStatus.COMPLETED,
1739
+ startedAt: startedAt,
1740
+ completedAt: new Date(),
1741
+ inputs: { mode: 'webhook', resumeToken, webhookPath },
1742
+ outputs: { waiting: true },
1743
+ error: null,
1744
+ retryCount: 0,
1745
+ });
1746
+ }
1747
+ return createStepResult(step.id, StepStatus.COMPLETED, {
1748
+ waiting: true,
1749
+ mode: 'webhook',
1750
+ resumeToken,
1751
+ webhookPath,
1752
+ }, startedAt);
1753
+ }
1754
+ case 'form': {
1755
+ if (!step.fields || Object.keys(step.fields).length === 0) {
1756
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Wait step with mode=form requires fields');
1757
+ }
1758
+ const resumeToken = crypto.randomUUID();
1759
+ if (this.stateStore) {
1760
+ this.stateStore.saveCheckpoint({
1761
+ runId: context.runId,
1762
+ stepIndex: context.currentStepIndex,
1763
+ stepName: step.id,
1764
+ status: StepStatus.COMPLETED,
1765
+ startedAt: startedAt,
1766
+ completedAt: new Date(),
1767
+ inputs: { mode: 'form', resumeToken, fields: step.fields },
1768
+ outputs: { waiting: true },
1769
+ error: null,
1770
+ retryCount: 0,
1771
+ });
1772
+ }
1773
+ return createStepResult(step.id, StepStatus.COMPLETED, {
1774
+ waiting: true,
1775
+ mode: 'form',
1776
+ resumeToken,
1777
+ fields: step.fields,
1778
+ formPath: `/form/${context.runId}/${step.id}/${resumeToken}`,
1779
+ }, startedAt);
1780
+ }
1781
+ default:
1782
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Unknown wait mode: ${step.mode}`);
1783
+ }
1784
+ }
1785
+ catch (error) {
1786
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1787
+ }
1788
+ }
1789
+ // ============================================================================
1790
+ // Merge Step Execution
1791
+ // ============================================================================
1792
+ /**
1793
+ * Execute a merge step - combines data from multiple sources.
1794
+ */
1795
+ async executeMergeStep(step, context) {
1796
+ const startedAt = new Date();
1797
+ try {
1798
+ // Resolve all source expressions to arrays
1799
+ const resolvedSources = [];
1800
+ for (const source of step.sources) {
1801
+ const resolved = resolveTemplates(source, context);
1802
+ if (!Array.isArray(resolved)) {
1803
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Merge source "${source}" did not resolve to an array`);
1804
+ }
1805
+ resolvedSources.push(resolved);
1806
+ }
1807
+ let result;
1808
+ switch (step.mode) {
1809
+ case 'append':
1810
+ result = resolvedSources.flat();
1811
+ break;
1812
+ case 'match': {
1813
+ if (!step.matchField) {
1814
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Merge mode "match" requires matchField');
1815
+ }
1816
+ // Return items that appear in ALL sources (by matchField)
1817
+ const fieldSets = resolvedSources.map((source) => new Set(source.map((item) => getField(item, step.matchField))));
1818
+ // Intersection of all field sets
1819
+ const commonKeys = fieldSets.reduce((acc, set) => new Set([...acc].filter((key) => set.has(key))));
1820
+ // Return items from first source that match
1821
+ result = resolvedSources[0].filter((item) => commonKeys.has(getField(item, step.matchField)));
1822
+ break;
1823
+ }
1824
+ case 'diff': {
1825
+ if (!step.matchField) {
1826
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Merge mode "diff" requires matchField');
1827
+ }
1828
+ // Return items from first source NOT found in other sources
1829
+ const otherKeys = new Set(resolvedSources.slice(1).flat().map((item) => getField(item, step.matchField)));
1830
+ result = resolvedSources[0].filter((item) => !otherKeys.has(getField(item, step.matchField)));
1831
+ break;
1832
+ }
1833
+ case 'combine_by_field': {
1834
+ if (!step.matchField) {
1835
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Merge mode "combine_by_field" requires matchField');
1836
+ }
1837
+ // Group items by matchField and merge their properties
1838
+ const grouped = new Map();
1839
+ const onConflict = step.onConflict ?? 'keep_last';
1840
+ for (const source of resolvedSources) {
1841
+ for (const item of source) {
1842
+ if (!item || typeof item !== 'object')
1843
+ continue;
1844
+ const key = getField(item, step.matchField);
1845
+ const existing = grouped.get(key);
1846
+ if (existing) {
1847
+ if (onConflict === 'keep_first') {
1848
+ // Only add new fields
1849
+ for (const [k, v] of Object.entries(item)) {
1850
+ if (!(k in existing))
1851
+ existing[k] = v;
1852
+ }
1853
+ }
1854
+ else if (onConflict === 'keep_last') {
1855
+ Object.assign(existing, item);
1856
+ }
1857
+ else {
1858
+ // merge_fields: deep merge
1859
+ Object.assign(existing, item);
1860
+ }
1861
+ }
1862
+ else {
1863
+ grouped.set(key, { ...item });
1864
+ }
1865
+ }
1866
+ }
1867
+ result = Array.from(grouped.values());
1868
+ break;
1869
+ }
1870
+ default:
1871
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Unknown merge mode: ${step.mode}`);
1872
+ }
1873
+ return createStepResult(step.id, StepStatus.COMPLETED, result, startedAt);
1874
+ }
1875
+ catch (error) {
1876
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1877
+ }
1878
+ }
1879
+ // ============================================================================
1439
1880
  // Helper Methods for Control Flow
1440
1881
  // ============================================================================
1441
1882
  /**