@marktoflow/core 2.0.0-alpha.7 → 2.0.1

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 (120) hide show
  1. package/README.md +24 -220
  2. package/dist/built-in-operations.d.ts +150 -0
  3. package/dist/built-in-operations.d.ts.map +1 -0
  4. package/dist/built-in-operations.js +799 -0
  5. package/dist/built-in-operations.js.map +1 -0
  6. package/dist/core-tools.d.ts +39 -0
  7. package/dist/core-tools.d.ts.map +1 -0
  8. package/dist/core-tools.js +58 -0
  9. package/dist/core-tools.js.map +1 -0
  10. package/dist/credentials.d.ts +60 -1
  11. package/dist/credentials.d.ts.map +1 -1
  12. package/dist/credentials.js +229 -4
  13. package/dist/credentials.js.map +1 -1
  14. package/dist/engine.d.ts +144 -3
  15. package/dist/engine.d.ts.map +1 -1
  16. package/dist/engine.js +1385 -49
  17. package/dist/engine.js.map +1 -1
  18. package/dist/file-operations.d.ts +86 -0
  19. package/dist/file-operations.d.ts.map +1 -0
  20. package/dist/file-operations.js +363 -0
  21. package/dist/file-operations.js.map +1 -0
  22. package/dist/index.d.ts +16 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +46 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/logging.d.ts +40 -2
  27. package/dist/logging.d.ts.map +1 -1
  28. package/dist/logging.js +166 -13
  29. package/dist/logging.js.map +1 -1
  30. package/dist/models.d.ts +1931 -203
  31. package/dist/models.d.ts.map +1 -1
  32. package/dist/models.js +303 -13
  33. package/dist/models.js.map +1 -1
  34. package/dist/nunjucks-filters.d.ts +271 -0
  35. package/dist/nunjucks-filters.d.ts.map +1 -0
  36. package/dist/nunjucks-filters.js +648 -0
  37. package/dist/nunjucks-filters.js.map +1 -0
  38. package/dist/oauth-manager.d.ts +128 -0
  39. package/dist/oauth-manager.d.ts.map +1 -0
  40. package/dist/oauth-manager.js +291 -0
  41. package/dist/oauth-manager.js.map +1 -0
  42. package/dist/oauth-refresh.d.ts +37 -0
  43. package/dist/oauth-refresh.d.ts.map +1 -0
  44. package/dist/oauth-refresh.js +76 -0
  45. package/dist/oauth-refresh.js.map +1 -0
  46. package/dist/parser.d.ts.map +1 -1
  47. package/dist/parser.js +291 -10
  48. package/dist/parser.js.map +1 -1
  49. package/dist/permissions.d.ts +49 -0
  50. package/dist/permissions.d.ts.map +1 -0
  51. package/dist/permissions.js +286 -0
  52. package/dist/permissions.js.map +1 -0
  53. package/dist/prompt-loader.d.ts +53 -0
  54. package/dist/prompt-loader.d.ts.map +1 -0
  55. package/dist/prompt-loader.js +205 -0
  56. package/dist/prompt-loader.js.map +1 -0
  57. package/dist/scheduler.d.ts +22 -3
  58. package/dist/scheduler.d.ts.map +1 -1
  59. package/dist/scheduler.js +72 -73
  60. package/dist/scheduler.js.map +1 -1
  61. package/dist/script-executor.d.ts +65 -0
  62. package/dist/script-executor.d.ts.map +1 -0
  63. package/dist/script-executor.js +261 -0
  64. package/dist/script-executor.js.map +1 -0
  65. package/dist/sdk-registry.d.ts +20 -2
  66. package/dist/sdk-registry.d.ts.map +1 -1
  67. package/dist/sdk-registry.js +100 -15
  68. package/dist/sdk-registry.js.map +1 -1
  69. package/dist/secret-providers/index.d.ts +12 -0
  70. package/dist/secret-providers/index.d.ts.map +1 -0
  71. package/dist/secret-providers/index.js +11 -0
  72. package/dist/secret-providers/index.js.map +1 -0
  73. package/dist/secret-providers/providers/aws.d.ts +32 -0
  74. package/dist/secret-providers/providers/aws.d.ts.map +1 -0
  75. package/dist/secret-providers/providers/aws.js +118 -0
  76. package/dist/secret-providers/providers/aws.js.map +1 -0
  77. package/dist/secret-providers/providers/azure.d.ts +40 -0
  78. package/dist/secret-providers/providers/azure.d.ts.map +1 -0
  79. package/dist/secret-providers/providers/azure.js +170 -0
  80. package/dist/secret-providers/providers/azure.js.map +1 -0
  81. package/dist/secret-providers/providers/env.d.ts +26 -0
  82. package/dist/secret-providers/providers/env.d.ts.map +1 -0
  83. package/dist/secret-providers/providers/env.js +59 -0
  84. package/dist/secret-providers/providers/env.js.map +1 -0
  85. package/dist/secret-providers/providers/vault.d.ts +39 -0
  86. package/dist/secret-providers/providers/vault.d.ts.map +1 -0
  87. package/dist/secret-providers/providers/vault.js +180 -0
  88. package/dist/secret-providers/providers/vault.js.map +1 -0
  89. package/dist/secret-providers/secret-manager.d.ts +72 -0
  90. package/dist/secret-providers/secret-manager.d.ts.map +1 -0
  91. package/dist/secret-providers/secret-manager.js +226 -0
  92. package/dist/secret-providers/secret-manager.js.map +1 -0
  93. package/dist/secret-providers/types.d.ts +105 -0
  94. package/dist/secret-providers/types.d.ts.map +1 -0
  95. package/dist/secret-providers/types.js +8 -0
  96. package/dist/secret-providers/types.js.map +1 -0
  97. package/dist/security.d.ts +1 -0
  98. package/dist/security.d.ts.map +1 -1
  99. package/dist/security.js +4 -0
  100. package/dist/security.js.map +1 -1
  101. package/dist/state.d.ts.map +1 -1
  102. package/dist/state.js +16 -9
  103. package/dist/state.js.map +1 -1
  104. package/dist/template-engine.d.ts +51 -0
  105. package/dist/template-engine.d.ts.map +1 -0
  106. package/dist/template-engine.js +227 -0
  107. package/dist/template-engine.js.map +1 -0
  108. package/dist/templates.d.ts +10 -0
  109. package/dist/templates.d.ts.map +1 -1
  110. package/dist/templates.js +21 -17
  111. package/dist/templates.js.map +1 -1
  112. package/dist/tools/mcp-tool.js +9 -9
  113. package/dist/tools/mcp-tool.js.map +1 -1
  114. package/dist/trigger-manager.js +1 -1
  115. package/dist/trigger-manager.js.map +1 -1
  116. package/dist/workflow-tools.d.ts +102 -0
  117. package/dist/workflow-tools.d.ts.map +1 -0
  118. package/dist/workflow-tools.js +130 -0
  119. package/dist/workflow-tools.js.map +1 -0
  120. package/package.json +24 -6
package/dist/engine.js CHANGED
@@ -4,10 +4,72 @@
4
4
  * Executes workflow steps with retry logic, variable resolution,
5
5
  * and SDK invocation.
6
6
  */
7
- import { StepStatus, WorkflowStatus, createExecutionContext, createStepResult, } 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
+ import { mergePermissions, toSecurityPolicy, } from './permissions.js';
9
+ import { loadPromptFile, resolvePromptTemplate, validatePromptInputs, } from './prompt-loader.js';
8
10
  import { DEFAULT_FAILOVER_CONFIG, AgentHealthTracker, FailoverReason, } from './failover.js';
9
11
  import { parseFile } from './parser.js';
10
12
  import { resolve, dirname } from 'node:path';
13
+ import { executeBuiltInOperation, isBuiltInOperation } from './built-in-operations.js';
14
+ import { renderTemplate } from './template-engine.js';
15
+ import { executeScriptAsync } from './script-executor.js';
16
+ // ============================================================================
17
+ // Helper Functions
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
+ }
41
+ /**
42
+ * Convert error to string for display/logging
43
+ */
44
+ function errorToString(error) {
45
+ if (!error)
46
+ return 'Unknown error';
47
+ if (typeof error === 'string')
48
+ return error;
49
+ if (error instanceof Error)
50
+ return error.message;
51
+ try {
52
+ return JSON.stringify(error);
53
+ }
54
+ catch {
55
+ return String(error);
56
+ }
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
+ }
11
73
  // ============================================================================
12
74
  // Retry Policy
13
75
  // ============================================================================
@@ -92,15 +154,25 @@ export class CircuitBreaker {
92
154
  // ============================================================================
93
155
  /**
94
156
  * Resolve template variables in a value.
95
- * Supports {{variable}} and {{inputs.name}} syntax.
157
+ * Supports {{variable}}, {{inputs.name}}, and Nunjucks filters.
158
+ *
159
+ * Uses Nunjucks as the template engine with:
160
+ * - Legacy regex operator support (=~, !~, //) converted to filters
161
+ * - Custom filters for string, array, object, date operations
162
+ * - Jinja2-style control flow ({% for %}, {% if %}, etc.)
96
163
  */
97
164
  export function resolveTemplates(value, context) {
98
165
  if (typeof value === 'string') {
99
- return value.replace(/\{\{([^}]+)\}\}/g, (_, varPath) => {
100
- const path = varPath.trim();
101
- const resolved = resolveVariablePath(path, context);
102
- return resolved !== undefined ? String(resolved) : '';
103
- });
166
+ // Build the template context with all available variables
167
+ // Spread inputs first, then variables (variables override inputs if same key)
168
+ // Also keep inputs accessible via inputs.* for explicit access
169
+ const templateContext = {
170
+ ...context.inputs, // Spread inputs at root level for direct access ({{ path }})
171
+ ...context.variables, // Variables override inputs if same key
172
+ inputs: context.inputs, // Also keep inputs accessible as inputs.*
173
+ };
174
+ // Use the new Nunjucks-based template engine with legacy syntax support
175
+ return renderTemplate(value, templateContext);
104
176
  }
105
177
  if (Array.isArray(value)) {
106
178
  return value.map((v) => resolveTemplates(v, context));
@@ -130,6 +202,11 @@ export function resolveVariablePath(path, context) {
130
202
  if (fromVars !== undefined) {
131
203
  return fromVars;
132
204
  }
205
+ // Check inputs (for bare variable names like "value" instead of "inputs.value")
206
+ const fromInputs = getNestedValue(context.inputs, path);
207
+ if (fromInputs !== undefined) {
208
+ return fromInputs;
209
+ }
133
210
  // Check step metadata (for status checks like: step_id.status)
134
211
  const fromStepMeta = getNestedValue(context.stepMetadata, path);
135
212
  if (fromStepMeta !== undefined) {
@@ -139,26 +216,63 @@ export function resolveVariablePath(path, context) {
139
216
  return getNestedValue(context, path);
140
217
  }
141
218
  /**
142
- * Get a nested value from an object using dot notation.
219
+ * Get a nested value from an object using dot notation and array indexing.
220
+ * Supports paths like: "user.name", "items[0].name", "data.users[1].email"
143
221
  */
144
222
  function getNestedValue(obj, path) {
145
223
  if (obj === null || obj === undefined) {
146
224
  return undefined;
147
225
  }
148
- const parts = path.split('.');
149
- let current = obj;
226
+ // Parse path into parts, handling both dot notation and array indexing
227
+ // Convert "a.b[0].c[1]" into ["a", "b", "0", "c", "1"]
228
+ const parts = [];
229
+ let current = '';
230
+ for (let i = 0; i < path.length; i++) {
231
+ const char = path[i];
232
+ if (char === '.') {
233
+ if (current) {
234
+ parts.push(current);
235
+ current = '';
236
+ }
237
+ }
238
+ else if (char === '[') {
239
+ if (current) {
240
+ parts.push(current);
241
+ current = '';
242
+ }
243
+ }
244
+ else if (char === ']') {
245
+ if (current) {
246
+ parts.push(current);
247
+ current = '';
248
+ }
249
+ }
250
+ else {
251
+ current += char;
252
+ }
253
+ }
254
+ if (current) {
255
+ parts.push(current);
256
+ }
257
+ // Traverse the object using the parsed parts
258
+ let result = obj;
150
259
  for (const part of parts) {
151
- if (current === null || current === undefined) {
260
+ if (result === null || result === undefined) {
152
261
  return undefined;
153
262
  }
154
- if (typeof current === 'object') {
155
- current = current[part];
263
+ // Check if part is a number (array index)
264
+ const index = Number(part);
265
+ if (!isNaN(index) && Array.isArray(result)) {
266
+ result = result[index];
267
+ }
268
+ else if (typeof result === 'object') {
269
+ result = result[part];
156
270
  }
157
271
  else {
158
272
  return undefined;
159
273
  }
160
274
  }
161
- return current;
275
+ return result;
162
276
  }
163
277
  export class WorkflowEngine {
164
278
  config;
@@ -170,13 +284,17 @@ export class WorkflowEngine {
170
284
  failoverConfig;
171
285
  healthTracker;
172
286
  failoverEvents = [];
173
- workflowPath; // Base path for resolving sub-workflows
287
+ workflowPath; // Base path for resolving sub-workflows (public for CLI state tracking)
288
+ workflowPermissions; // Workflow-level permissions
289
+ promptCache = new Map(); // Cache for loaded prompts
174
290
  constructor(config = {}, events = {}, stateStore) {
175
291
  this.config = {
176
292
  defaultTimeout: config.defaultTimeout ?? 60000,
177
293
  maxRetries: config.maxRetries ?? 3,
178
294
  retryBaseDelay: config.retryBaseDelay ?? 1000,
179
295
  retryMaxDelay: config.retryMaxDelay ?? 30000,
296
+ defaultAgent: config.defaultAgent,
297
+ defaultModel: config.defaultModel,
180
298
  };
181
299
  this.retryPolicy = new RetryPolicy(this.config.maxRetries, this.config.retryBaseDelay, this.config.retryMaxDelay);
182
300
  this.events = events;
@@ -185,6 +303,54 @@ export class WorkflowEngine {
185
303
  this.failoverConfig = { ...DEFAULT_FAILOVER_CONFIG, ...(config.failoverConfig ?? {}) };
186
304
  this.healthTracker = config.healthTracker ?? new AgentHealthTracker();
187
305
  }
306
+ /**
307
+ * Execute a single step - dispatcher to specialized execution methods.
308
+ */
309
+ async executeStep(step, context, sdkRegistry, stepExecutor) {
310
+ // Check conditions first (applies to all step types)
311
+ if (step.conditions && !this.evaluateConditions(step.conditions, context)) {
312
+ return createStepResult(step.id, StepStatus.SKIPPED, null, new Date());
313
+ }
314
+ // Dispatch to specialized execution method based on step type
315
+ if (isIfStep(step)) {
316
+ return this.executeIfStep(step, context, sdkRegistry, stepExecutor);
317
+ }
318
+ if (isSwitchStep(step)) {
319
+ return this.executeSwitchStep(step, context, sdkRegistry, stepExecutor);
320
+ }
321
+ if (isForEachStep(step)) {
322
+ return this.executeForEachStep(step, context, sdkRegistry, stepExecutor);
323
+ }
324
+ if (isWhileStep(step)) {
325
+ return this.executeWhileStep(step, context, sdkRegistry, stepExecutor);
326
+ }
327
+ if (isMapStep(step)) {
328
+ return this.executeMapStep(step, context, sdkRegistry, stepExecutor);
329
+ }
330
+ if (isFilterStep(step)) {
331
+ return this.executeFilterStep(step, context, sdkRegistry, stepExecutor);
332
+ }
333
+ if (isReduceStep(step)) {
334
+ return this.executeReduceStep(step, context, sdkRegistry, stepExecutor);
335
+ }
336
+ if (isParallelStep(step)) {
337
+ return this.executeParallelStep(step, context, sdkRegistry, stepExecutor);
338
+ }
339
+ if (isTryStep(step)) {
340
+ return this.executeTryStep(step, context, sdkRegistry, stepExecutor);
341
+ }
342
+ if (isScriptStep(step)) {
343
+ return this.executeScriptStep(step, context);
344
+ }
345
+ if (isWaitStep(step)) {
346
+ return this.executeWaitStep(step, context);
347
+ }
348
+ if (isMergeStep(step)) {
349
+ return this.executeMergeStep(step, context);
350
+ }
351
+ // Default: action or workflow step
352
+ return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
353
+ }
188
354
  /**
189
355
  * Execute a workflow.
190
356
  */
@@ -192,13 +358,22 @@ export class WorkflowEngine {
192
358
  const context = createExecutionContext(workflow, inputs);
193
359
  const stepResults = [];
194
360
  const startedAt = new Date();
361
+ // Store workflow-level permissions and defaults
362
+ this.workflowPermissions = workflow.permissions;
363
+ // Use workflow defaults if not set in engine config
364
+ if (!this.config.defaultAgent && workflow.defaultAgent) {
365
+ this.config.defaultAgent = workflow.defaultAgent;
366
+ }
367
+ if (!this.config.defaultModel && workflow.defaultModel) {
368
+ this.config.defaultModel = workflow.defaultModel;
369
+ }
195
370
  context.status = WorkflowStatus.RUNNING;
196
371
  this.events.onWorkflowStart?.(workflow, context);
197
372
  if (this.stateStore) {
198
373
  this.stateStore.createExecution({
199
374
  runId: context.runId,
200
375
  workflowId: workflow.metadata.id,
201
- workflowPath: 'unknown',
376
+ workflowPath: this.workflowPath ?? 'unknown',
202
377
  status: WorkflowStatus.RUNNING,
203
378
  startedAt: startedAt,
204
379
  completedAt: null,
@@ -214,32 +389,39 @@ export class WorkflowEngine {
214
389
  for (let i = 0; i < workflow.steps.length; i++) {
215
390
  const step = workflow.steps[i];
216
391
  context.currentStepIndex = i;
217
- // Check conditions
218
- if (step.conditions && !this.evaluateConditions(step.conditions, context)) {
219
- const skipResult = createStepResult(step.id, StepStatus.SKIPPED, null, new Date());
220
- stepResults.push(skipResult);
221
- continue;
222
- }
223
- // Execute step with retry
224
- const result = await this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
392
+ // Execute step using dispatcher
393
+ const result = await this.executeStep(step, context, sdkRegistry, stepExecutor);
225
394
  stepResults.push(result);
226
395
  // Store step metadata (status, error, etc.) in separate field for condition evaluation
227
396
  // This allows conditions like: step_id.status == 'failed'
228
397
  context.stepMetadata[step.id] = {
229
398
  status: result.status.toLowerCase(),
230
399
  retryCount: result.retryCount,
231
- ...(result.error ? { error: result.error } : {}),
400
+ ...(result.error ? { error: errorToString(result.error) } : {}),
232
401
  };
233
402
  // Store output variable
234
403
  if (step.outputVariable && result.status === StepStatus.COMPLETED) {
235
404
  context.variables[step.outputVariable] = result.output;
236
405
  }
406
+ // Check if this step set workflow outputs (from workflow.set_outputs action)
407
+ if (result.status === StepStatus.COMPLETED &&
408
+ result.output &&
409
+ typeof result.output === 'object' &&
410
+ '__workflow_outputs__' in result.output) {
411
+ const outputObj = result.output;
412
+ const outputs = outputObj['__workflow_outputs__'];
413
+ context.workflowOutputs = outputs;
414
+ }
237
415
  // Handle failure
238
416
  if (result.status === StepStatus.FAILED) {
239
- const errorAction = step.errorHandling?.action ?? 'stop';
417
+ // Get error action from step if it has error handling
418
+ let errorAction = 'stop';
419
+ if ('errorHandling' in step && step.errorHandling?.action) {
420
+ errorAction = step.errorHandling.action;
421
+ }
240
422
  if (errorAction === 'stop') {
241
423
  context.status = WorkflowStatus.FAILED;
242
- const workflowError = result.error || `Step ${step.id} failed`;
424
+ const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
243
425
  const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
244
426
  this.events.onWorkflowComplete?.(workflow, workflowResult);
245
427
  return workflowResult;
@@ -254,7 +436,7 @@ export class WorkflowEngine {
254
436
  });
255
437
  }
256
438
  context.status = WorkflowStatus.FAILED;
257
- const workflowError = result.error || `Step ${step.id} failed`;
439
+ const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
258
440
  const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
259
441
  this.events.onWorkflowComplete?.(workflow, workflowResult);
260
442
  return workflowResult;
@@ -288,6 +470,136 @@ export class WorkflowEngine {
288
470
  this.events.onWorkflowComplete?.(workflow, workflowResult);
289
471
  return workflowResult;
290
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
+ }
291
603
  /**
292
604
  * Execute a workflow from a file.
293
605
  * This method automatically sets the workflow path for resolving sub-workflows.
@@ -307,8 +619,8 @@ export class WorkflowEngine {
307
619
  * Execute a sub-workflow.
308
620
  */
309
621
  async executeSubWorkflow(step, context, sdkRegistry, stepExecutor) {
310
- if (!step.workflow) {
311
- throw new Error(`Step ${step.id} has no workflow path`);
622
+ if (!isSubWorkflowStep(step)) {
623
+ throw new Error(`Step ${step.id} is not a workflow step`);
312
624
  }
313
625
  // Resolve the sub-workflow path relative to the parent workflow
314
626
  const subWorkflowPath = this.workflowPath
@@ -342,15 +654,302 @@ export class WorkflowEngine {
342
654
  // Return the sub-workflow output
343
655
  return result.output;
344
656
  }
657
+ /**
658
+ * Execute a sub-workflow using an AI sub-agent.
659
+ * The agent interprets the workflow and executes it autonomously.
660
+ */
661
+ async executeSubWorkflowWithAgent(step, context, sdkRegistry, stepExecutor) {
662
+ // Resolve the sub-workflow path
663
+ const subWorkflowPath = this.workflowPath
664
+ ? resolve(dirname(this.workflowPath), step.workflow)
665
+ : resolve(step.workflow);
666
+ // Read the workflow file content
667
+ const { readFile } = await import('node:fs/promises');
668
+ const workflowContent = await readFile(subWorkflowPath, 'utf-8');
669
+ // Resolve inputs for the sub-workflow
670
+ const resolvedInputs = resolveTemplates(step.inputs, context);
671
+ // Get subagent configuration
672
+ const subagentConfig = step.subagentConfig || {};
673
+ const model = subagentConfig.model || step.model || this.config.defaultModel;
674
+ const maxTurns = subagentConfig.maxTurns || 10;
675
+ const systemPrompt = subagentConfig.systemPrompt || this.buildDefaultSubagentSystemPrompt();
676
+ const tools = subagentConfig.tools || ['Read', 'Write', 'Bash', 'Glob', 'Grep'];
677
+ // Build the prompt for the agent
678
+ const agentPrompt = this.buildSubagentPrompt(workflowContent, resolvedInputs, tools);
679
+ // Determine the agent action to use
680
+ const agentName = step.agent || this.config.defaultAgent || 'agent';
681
+ const agentAction = `${agentName}.chat.completions`;
682
+ // Build the messages array
683
+ const messages = [
684
+ { role: 'system', content: systemPrompt },
685
+ { role: 'user', content: agentPrompt },
686
+ ];
687
+ // Create a virtual action step to execute via the agent
688
+ const agentStep = {
689
+ id: `${step.id}-subagent`,
690
+ type: 'action',
691
+ action: agentAction,
692
+ inputs: {
693
+ model,
694
+ messages,
695
+ max_tokens: 8192,
696
+ },
697
+ model,
698
+ agent: agentName,
699
+ };
700
+ // Build executor context
701
+ const executorContext = this.buildStepExecutorContext(agentStep);
702
+ // Execute the agent call
703
+ let response;
704
+ let turns = 0;
705
+ let conversationMessages = [...messages];
706
+ let finalOutput = {};
707
+ while (turns < maxTurns) {
708
+ turns++;
709
+ try {
710
+ response = await stepExecutor({ ...agentStep, inputs: { ...agentStep.inputs, messages: conversationMessages } }, context, sdkRegistry, executorContext);
711
+ // Parse the response
712
+ const parsedResponse = this.parseSubagentResponse(response);
713
+ if (parsedResponse.completed) {
714
+ finalOutput = parsedResponse.output || {};
715
+ break;
716
+ }
717
+ // If agent needs to continue, add its response and continue
718
+ if (parsedResponse.message) {
719
+ conversationMessages.push({ role: 'assistant', content: parsedResponse.message });
720
+ // Agent might request a tool call - for now, we'll prompt it to continue
721
+ conversationMessages.push({ role: 'user', content: 'Continue with the workflow execution.' });
722
+ }
723
+ else {
724
+ // No clear continuation, assume completed
725
+ finalOutput = parsedResponse.output || {};
726
+ break;
727
+ }
728
+ }
729
+ catch (error) {
730
+ throw new Error(`Sub-agent execution failed at turn ${turns}: ${error instanceof Error ? error.message : String(error)}`);
731
+ }
732
+ }
733
+ if (turns >= maxTurns) {
734
+ throw new Error(`Sub-agent exceeded maximum turns (${maxTurns})`);
735
+ }
736
+ return finalOutput;
737
+ }
738
+ /**
739
+ * Build the default system prompt for sub-agent execution.
740
+ */
741
+ buildDefaultSubagentSystemPrompt() {
742
+ return `You are an AI agent executing a workflow. Your task is to interpret the workflow definition and execute each step in order.
743
+
744
+ For each step:
745
+ 1. Understand what the step requires
746
+ 2. Execute the action described
747
+ 3. Store any outputs as specified
748
+
749
+ When you complete all steps, respond with a JSON object containing the workflow outputs.
750
+
751
+ Format your final response as:
752
+ \`\`\`json
753
+ {
754
+ "completed": true,
755
+ "output": { /* workflow outputs here */ }
756
+ }
757
+ \`\`\`
758
+
759
+ If you encounter an error, respond with:
760
+ \`\`\`json
761
+ {
762
+ "completed": false,
763
+ "error": "description of the error"
764
+ }
765
+ \`\`\``;
766
+ }
767
+ /**
768
+ * Build the prompt for sub-agent workflow execution.
769
+ */
770
+ buildSubagentPrompt(workflowContent, inputs, tools) {
771
+ return `Execute the following workflow:
772
+
773
+ ## Workflow Definition
774
+ \`\`\`markdown
775
+ ${workflowContent}
776
+ \`\`\`
777
+
778
+ ## Inputs
779
+ \`\`\`json
780
+ ${JSON.stringify(inputs, null, 2)}
781
+ \`\`\`
782
+
783
+ ## Available Tools
784
+ ${tools.join(', ')}
785
+
786
+ Execute the workflow steps in order and return the final outputs as JSON.`;
787
+ }
788
+ /**
789
+ * Parse the sub-agent's response to extract completion status and output.
790
+ */
791
+ parseSubagentResponse(response) {
792
+ // Try to extract content from various response formats
793
+ let content;
794
+ if (typeof response === 'string') {
795
+ content = response;
796
+ }
797
+ else if (response && typeof response === 'object') {
798
+ const resp = response;
799
+ // OpenAI-style response
800
+ if (resp.choices && Array.isArray(resp.choices)) {
801
+ const choice = resp.choices[0];
802
+ if (choice.message && typeof choice.message === 'object') {
803
+ const message = choice.message;
804
+ content = message.content;
805
+ }
806
+ }
807
+ // Anthropic-style response
808
+ else if (resp.content && Array.isArray(resp.content)) {
809
+ const textBlock = resp.content.find((c) => typeof c === 'object' && c !== null && c.type === 'text');
810
+ content = textBlock?.text;
811
+ }
812
+ // Direct content field
813
+ else if (typeof resp.content === 'string') {
814
+ content = resp.content;
815
+ }
816
+ // Direct message field
817
+ else if (typeof resp.message === 'string') {
818
+ content = resp.message;
819
+ }
820
+ }
821
+ if (!content) {
822
+ return { completed: false, message: 'No content in response' };
823
+ }
824
+ // Try to parse JSON from the response
825
+ const jsonMatch = content.match(/```json\n?([\s\S]*?)```/);
826
+ if (jsonMatch) {
827
+ try {
828
+ const parsed = JSON.parse(jsonMatch[1]);
829
+ const output = parsed.output;
830
+ const error = parsed.error;
831
+ return {
832
+ completed: parsed.completed === true,
833
+ ...(output !== undefined ? { output } : {}),
834
+ ...(error !== undefined ? { error } : {}),
835
+ };
836
+ }
837
+ catch (e) {
838
+ console.warn('[marktoflow] Failed to parse JSON block in sub-agent response:', e.message);
839
+ }
840
+ }
841
+ // Try to parse raw JSON
842
+ try {
843
+ const parsed = JSON.parse(content);
844
+ if (typeof parsed.completed === 'boolean') {
845
+ const output = parsed.output;
846
+ const error = parsed.error;
847
+ return {
848
+ completed: parsed.completed,
849
+ ...(output !== undefined ? { output } : {}),
850
+ ...(error !== undefined ? { error } : {}),
851
+ };
852
+ }
853
+ }
854
+ catch (e) {
855
+ console.warn('[marktoflow] Sub-agent response is not valid JSON:', e.message);
856
+ }
857
+ // Return the content as a message
858
+ return { completed: false, message: content };
859
+ }
860
+ /**
861
+ * Build the step executor context with effective model/agent/permissions.
862
+ */
863
+ buildStepExecutorContext(step) {
864
+ // Merge workflow and step permissions
865
+ const effectivePermissions = mergePermissions(this.workflowPermissions, step.permissions);
866
+ // Resolve effective model/agent (step overrides workflow defaults)
867
+ const effectiveModel = step.model || this.config.defaultModel;
868
+ const effectiveAgent = step.agent || this.config.defaultAgent;
869
+ return {
870
+ model: effectiveModel,
871
+ agent: effectiveAgent,
872
+ permissions: effectivePermissions,
873
+ securityPolicy: toSecurityPolicy(effectivePermissions),
874
+ basePath: this.workflowPath,
875
+ };
876
+ }
877
+ /**
878
+ * Load and resolve an external prompt file for a step.
879
+ */
880
+ async loadAndResolvePrompt(step, context) {
881
+ if (!step.prompt) {
882
+ return step.inputs;
883
+ }
884
+ // Check cache
885
+ let loadedPrompt = this.promptCache.get(step.prompt);
886
+ if (!loadedPrompt) {
887
+ loadedPrompt = await loadPromptFile(step.prompt, this.workflowPath);
888
+ this.promptCache.set(step.prompt, loadedPrompt);
889
+ }
890
+ // Resolve prompt inputs (from step.promptInputs, with template resolution)
891
+ const promptInputs = step.promptInputs
892
+ ? resolveTemplates(step.promptInputs, context)
893
+ : {};
894
+ // Validate prompt inputs
895
+ const validation = validatePromptInputs(loadedPrompt, promptInputs);
896
+ if (!validation.valid) {
897
+ throw new Error(`Invalid prompt inputs: ${validation.errors.join(', ')}`);
898
+ }
899
+ // Resolve the prompt template
900
+ const resolved = resolvePromptTemplate(loadedPrompt, promptInputs, context);
901
+ // Merge resolved prompt content into inputs
902
+ // The resolved content typically goes into a 'messages' or 'prompt' field
903
+ const resolvedInputs = { ...step.inputs };
904
+ // If inputs has a 'messages' array with a user message, inject prompt content
905
+ if (Array.isArray(resolvedInputs.messages)) {
906
+ resolvedInputs.messages = resolvedInputs.messages.map((msg) => {
907
+ if (typeof msg === 'object' && msg !== null) {
908
+ const message = msg;
909
+ if (message.role === 'user' && typeof message.content === 'string') {
910
+ // Replace {{ prompt }} placeholder with resolved content
911
+ return {
912
+ ...message,
913
+ content: message.content.replace(/\{\{\s*prompt\s*\}\}/g, resolved.content),
914
+ };
915
+ }
916
+ }
917
+ return msg;
918
+ });
919
+ }
920
+ else {
921
+ // Add resolved prompt as 'promptContent' for the executor to use
922
+ resolvedInputs.promptContent = resolved.content;
923
+ }
924
+ return resolvedInputs;
925
+ }
345
926
  /**
346
927
  * Execute a step with retry logic.
347
928
  */
348
929
  async executeStepWithRetry(step, context, sdkRegistry, stepExecutor) {
349
- const maxRetries = step.errorHandling?.maxRetries ?? this.config.maxRetries;
350
930
  const startedAt = new Date();
351
931
  let lastError;
932
+ // Build executor context with model/agent/permissions
933
+ const executorContext = this.buildStepExecutorContext(step);
352
934
  // Handle sub-workflow execution
353
- if (step.workflow) {
935
+ if (isSubWorkflowStep(step)) {
936
+ // Check if we should use subagent execution
937
+ if (step.useSubagent) {
938
+ try {
939
+ this.events.onStepStart?.(step, context);
940
+ const output = await this.executeWithTimeout(() => this.executeSubWorkflowWithAgent(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
941
+ const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, 0);
942
+ this.events.onStepComplete?.(step, result);
943
+ return result;
944
+ }
945
+ catch (error) {
946
+ lastError = error instanceof Error ? error : new Error(String(error));
947
+ const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError);
948
+ this.events.onStepComplete?.(step, result);
949
+ return result;
950
+ }
951
+ }
952
+ // Standard sub-workflow execution
354
953
  try {
355
954
  this.events.onStepStart?.(step, context);
356
955
  const output = await this.executeWithTimeout(() => this.executeSubWorkflow(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
@@ -360,15 +959,17 @@ export class WorkflowEngine {
360
959
  }
361
960
  catch (error) {
362
961
  lastError = error instanceof Error ? error : new Error(String(error));
363
- const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError.message);
962
+ const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, lastError // Pass full error object
963
+ );
364
964
  this.events.onStepComplete?.(step, result);
365
965
  return result;
366
966
  }
367
967
  }
368
968
  // Regular action step - ensure action is defined
369
- if (!step.action) {
370
- return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Step has neither action nor workflow defined');
969
+ if (!isActionStep(step)) {
970
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Step is neither an action nor a workflow');
371
971
  }
972
+ const maxRetries = step.errorHandling?.maxRetries ?? this.config.maxRetries;
372
973
  // Get or create circuit breaker for this step's action
373
974
  const [serviceName] = step.action.split('.');
374
975
  let circuitBreaker = this.circuitBreakers.get(serviceName);
@@ -383,11 +984,30 @@ export class WorkflowEngine {
383
984
  }
384
985
  this.events.onStepStart?.(step, context);
385
986
  try {
386
- // Resolve templates in inputs
387
- const resolvedInputs = resolveTemplates(step.inputs, context);
987
+ // Load and resolve external prompt if specified
988
+ let resolvedInputs;
989
+ if (step.prompt) {
990
+ resolvedInputs = await this.loadAndResolvePrompt(step, context);
991
+ resolvedInputs = resolveTemplates(resolvedInputs, context);
992
+ }
993
+ else {
994
+ // Resolve templates in inputs
995
+ resolvedInputs = resolveTemplates(step.inputs, context);
996
+ }
388
997
  const stepWithResolvedInputs = { ...step, inputs: resolvedInputs };
389
- // Execute step
390
- const output = await this.executeWithTimeout(() => stepExecutor(stepWithResolvedInputs, context, sdkRegistry), step.timeout ?? this.config.defaultTimeout);
998
+ // Check if this is a built-in operation
999
+ let output;
1000
+ if (isBuiltInOperation(step.action)) {
1001
+ // Execute built-in operation directly (no timeout, no SDK executor needed)
1002
+ // For built-in operations, pass both resolved and unresolved inputs
1003
+ // to allow selective resolution of template expressions
1004
+ // Await in case operation is async (e.g., file operations)
1005
+ output = await executeBuiltInOperation(step.action, step.inputs, resolvedInputs, context);
1006
+ }
1007
+ else {
1008
+ // Execute step with executor context
1009
+ output = await this.executeWithTimeout(() => stepExecutor(stepWithResolvedInputs, context, sdkRegistry, executorContext), step.timeout ?? this.config.defaultTimeout);
1010
+ }
391
1011
  circuitBreaker.recordSuccess();
392
1012
  const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, attempt);
393
1013
  this.events.onStepComplete?.(step, result);
@@ -405,7 +1025,8 @@ export class WorkflowEngine {
405
1025
  }
406
1026
  }
407
1027
  // All retries exhausted
408
- const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, maxRetries, lastError?.message);
1028
+ const result = createStepResult(step.id, StepStatus.FAILED, null, startedAt, maxRetries, lastError // Pass full error object to preserve HTTP details, stack traces, etc.
1029
+ );
409
1030
  this.events.onStepComplete?.(step, result);
410
1031
  return result;
411
1032
  }
@@ -414,8 +1035,8 @@ export class WorkflowEngine {
414
1035
  */
415
1036
  async executeStepWithFailover(step, context, sdkRegistry, stepExecutor) {
416
1037
  const primaryResult = await this.executeStepWithRetry(step, context, sdkRegistry, stepExecutor);
417
- // Sub-workflows don't support failover
418
- if (step.workflow || !step.action) {
1038
+ // Sub-workflows and non-action steps don't support failover
1039
+ if (!isActionStep(step)) {
419
1040
  return primaryResult;
420
1041
  }
421
1042
  const [primaryTool, ...methodParts] = step.action.split('.');
@@ -424,7 +1045,7 @@ export class WorkflowEngine {
424
1045
  this.healthTracker.markHealthy(primaryTool);
425
1046
  return primaryResult;
426
1047
  }
427
- const errorMessage = primaryResult.error ?? '';
1048
+ const errorMessage = primaryResult.error ? errorToString(primaryResult.error) : '';
428
1049
  const isTimeout = errorMessage.includes('timed out');
429
1050
  if (isTimeout && !this.failoverConfig.failoverOnTimeout) {
430
1051
  this.healthTracker.markUnhealthy(primaryTool, errorMessage);
@@ -444,7 +1065,7 @@ export class WorkflowEngine {
444
1065
  continue;
445
1066
  if (attempts >= this.failoverConfig.maxFailoverAttempts)
446
1067
  break;
447
- const fallbackStep = { ...step, action: `${fallbackTool}.${method}` };
1068
+ const fallbackStep = { ...step, action: `${fallbackTool}.${method}`, type: 'action' };
448
1069
  const result = await this.executeStepWithRetry(fallbackStep, context, sdkRegistry, stepExecutor);
449
1070
  this.failoverEvents.push({
450
1071
  timestamp: new Date(),
@@ -528,12 +1149,27 @@ export class WorkflowEngine {
528
1149
  /**
529
1150
  * Resolve a condition value with support for nested properties.
530
1151
  * Handles direct variable references and nested paths.
1152
+ * Uses Nunjucks for template expressions with filters/regex.
531
1153
  */
532
1154
  resolveConditionValue(path, context) {
533
- // Try to resolve the path directly from variables
534
- const resolved = resolveVariablePath(path, context);
535
- // Return the resolved value directly
536
- return resolved;
1155
+ // If it looks like a template expression, resolve it
1156
+ if (path.includes('|') || path.includes('=~') || path.includes('!~')) {
1157
+ // Build template context
1158
+ const templateContext = {
1159
+ inputs: context.inputs,
1160
+ ...context.variables,
1161
+ };
1162
+ return renderTemplate(`{{ ${path} }}`, templateContext);
1163
+ }
1164
+ // First try to parse as a literal value (true, false, numbers, etc.)
1165
+ const parsedValue = this.parseValue(path);
1166
+ // If parseValue returned the same string, try to resolve as a variable
1167
+ if (parsedValue === path) {
1168
+ const resolved = resolveVariablePath(path, context);
1169
+ return resolved;
1170
+ }
1171
+ // Return the parsed literal value
1172
+ return parsedValue;
537
1173
  }
538
1174
  /**
539
1175
  * Parse a value from a condition string.
@@ -562,12 +1198,14 @@ export class WorkflowEngine {
562
1198
  */
563
1199
  buildWorkflowResult(workflow, context, stepResults, startedAt, error) {
564
1200
  const completedAt = new Date();
1201
+ // Use workflowOutputs if set by workflow.set_outputs, otherwise use all variables
1202
+ const output = context.workflowOutputs || context.variables;
565
1203
  return {
566
1204
  workflowId: workflow.metadata.id,
567
1205
  runId: context.runId,
568
1206
  status: context.status,
569
1207
  stepResults,
570
- output: context.variables,
1208
+ output,
571
1209
  error,
572
1210
  startedAt,
573
1211
  completedAt,
@@ -582,6 +1220,704 @@ export class WorkflowEngine {
582
1220
  breaker.reset();
583
1221
  }
584
1222
  }
1223
+ // ============================================================================
1224
+ // Control Flow Execution Methods
1225
+ // ============================================================================
1226
+ /**
1227
+ * Execute an if/else conditional step.
1228
+ */
1229
+ async executeIfStep(step, context, sdkRegistry, stepExecutor) {
1230
+ const startedAt = new Date();
1231
+ try {
1232
+ // Evaluate condition
1233
+ const conditionResult = this.evaluateCondition(step.condition, context);
1234
+ // Determine which branch to execute
1235
+ const branchSteps = conditionResult
1236
+ ? step.then || step.steps // 'steps' is alias for 'then'
1237
+ : step.else;
1238
+ if (!branchSteps || branchSteps.length === 0) {
1239
+ return createStepResult(step.id, StepStatus.SKIPPED, null, startedAt);
1240
+ }
1241
+ // Execute the branch steps
1242
+ const branchResults = [];
1243
+ for (const branchStep of branchSteps) {
1244
+ const result = await this.executeStep(branchStep, context, sdkRegistry, stepExecutor);
1245
+ if (result.status === StepStatus.COMPLETED && branchStep.outputVariable) {
1246
+ context.variables[branchStep.outputVariable] = result.output;
1247
+ branchResults.push(result.output);
1248
+ }
1249
+ if (result.status === StepStatus.FAILED) {
1250
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
1251
+ }
1252
+ }
1253
+ return createStepResult(step.id, StepStatus.COMPLETED, branchResults, startedAt);
1254
+ }
1255
+ catch (error) {
1256
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1257
+ }
1258
+ }
1259
+ /**
1260
+ * Execute a switch/case step.
1261
+ */
1262
+ async executeSwitchStep(step, context, sdkRegistry, stepExecutor) {
1263
+ const startedAt = new Date();
1264
+ try {
1265
+ // Resolve the switch expression
1266
+ const expressionValue = String(resolveTemplates(step.expression, context));
1267
+ // Find matching case
1268
+ const caseSteps = step.cases[expressionValue] || step.default;
1269
+ if (!caseSteps || caseSteps.length === 0) {
1270
+ return createStepResult(step.id, StepStatus.SKIPPED, null, startedAt);
1271
+ }
1272
+ // Execute case steps
1273
+ const caseResults = [];
1274
+ for (const caseStep of caseSteps) {
1275
+ const result = await this.executeStep(caseStep, context, sdkRegistry, stepExecutor);
1276
+ if (result.status === StepStatus.COMPLETED && caseStep.outputVariable) {
1277
+ context.variables[caseStep.outputVariable] = result.output;
1278
+ caseResults.push(result.output);
1279
+ }
1280
+ if (result.status === StepStatus.FAILED) {
1281
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
1282
+ }
1283
+ }
1284
+ return createStepResult(step.id, StepStatus.COMPLETED, caseResults, startedAt);
1285
+ }
1286
+ catch (error) {
1287
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Execute a for-each loop step.
1292
+ * Supports optional batch_size and pause_between_batches for rate limiting.
1293
+ */
1294
+ async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
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
+ };
1303
+ try {
1304
+ // Resolve items array
1305
+ const items = resolveTemplates(step.items, context);
1306
+ if (!Array.isArray(items)) {
1307
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
1308
+ }
1309
+ if (items.length === 0) {
1310
+ return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
1311
+ }
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
1319
+ const results = [];
1320
+ for (let i = 0; i < items.length; i++) {
1321
+ context.variables[step.itemVariable] = items[i];
1322
+ context.variables['loop'] = {
1323
+ index: i,
1324
+ first: i === 0,
1325
+ last: i === items.length - 1,
1326
+ length: items.length,
1327
+ };
1328
+ if (step.indexVariable) {
1329
+ context.variables[step.indexVariable] = i;
1330
+ }
1331
+ // Execute iteration steps
1332
+ for (const iterStep of step.steps) {
1333
+ const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
1334
+ if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
1335
+ context.variables[iterStep.outputVariable] = result.output;
1336
+ }
1337
+ if (result.status === StepStatus.FAILED) {
1338
+ const errorAction = step.errorHandling?.action ?? 'stop';
1339
+ if (errorAction === 'stop') {
1340
+ cleanupLoopVars();
1341
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
1342
+ }
1343
+ break;
1344
+ }
1345
+ }
1346
+ results.push(context.variables[step.itemVariable]);
1347
+ }
1348
+ cleanupLoopVars();
1349
+ return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
1350
+ }
1351
+ catch (error) {
1352
+ cleanupLoopVars();
1353
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1354
+ }
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
+ }
1416
+ /**
1417
+ * Execute a while loop step.
1418
+ */
1419
+ async executeWhileStep(step, context, sdkRegistry, stepExecutor) {
1420
+ const startedAt = new Date();
1421
+ let iterations = 0;
1422
+ try {
1423
+ while (this.evaluateCondition(step.condition, context)) {
1424
+ if (iterations >= step.maxIterations) {
1425
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, `Max iterations (${step.maxIterations}) exceeded`);
1426
+ }
1427
+ // Execute iteration steps
1428
+ for (const iterStep of step.steps) {
1429
+ const result = await this.executeStep(iterStep, context, sdkRegistry, stepExecutor);
1430
+ if (result.status === StepStatus.COMPLETED && iterStep.outputVariable) {
1431
+ context.variables[iterStep.outputVariable] = result.output;
1432
+ }
1433
+ if (result.status === StepStatus.FAILED) {
1434
+ const errorAction = step.errorHandling?.action ?? 'stop';
1435
+ if (errorAction === 'stop') {
1436
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
1437
+ }
1438
+ // 'continue' - skip to next iteration
1439
+ break;
1440
+ }
1441
+ }
1442
+ iterations++;
1443
+ }
1444
+ return createStepResult(step.id, StepStatus.COMPLETED, { iterations }, startedAt);
1445
+ }
1446
+ catch (error) {
1447
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Execute a map transformation step.
1452
+ */
1453
+ async executeMapStep(step, context, _sdkRegistry, _stepExecutor) {
1454
+ const startedAt = new Date();
1455
+ try {
1456
+ // Resolve items array
1457
+ const items = resolveTemplates(step.items, context);
1458
+ if (!Array.isArray(items)) {
1459
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
1460
+ }
1461
+ // Map each item using the expression
1462
+ const mapped = items.map((item) => {
1463
+ context.variables[step.itemVariable] = item;
1464
+ const result = resolveTemplates(step.expression, context);
1465
+ delete context.variables[step.itemVariable];
1466
+ return result;
1467
+ });
1468
+ return createStepResult(step.id, StepStatus.COMPLETED, mapped, startedAt);
1469
+ }
1470
+ catch (error) {
1471
+ delete context.variables[step.itemVariable];
1472
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1473
+ }
1474
+ }
1475
+ /**
1476
+ * Execute a filter step.
1477
+ */
1478
+ async executeFilterStep(step, context, _sdkRegistry, _stepExecutor) {
1479
+ const startedAt = new Date();
1480
+ try {
1481
+ // Resolve items array
1482
+ const items = resolveTemplates(step.items, context);
1483
+ if (!Array.isArray(items)) {
1484
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
1485
+ }
1486
+ // Filter items using the condition
1487
+ const filtered = items.filter((item) => {
1488
+ context.variables[step.itemVariable] = item;
1489
+ const result = this.evaluateCondition(step.condition, context);
1490
+ delete context.variables[step.itemVariable];
1491
+ return result;
1492
+ });
1493
+ return createStepResult(step.id, StepStatus.COMPLETED, filtered, startedAt);
1494
+ }
1495
+ catch (error) {
1496
+ delete context.variables[step.itemVariable];
1497
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1498
+ }
1499
+ }
1500
+ /**
1501
+ * Execute a reduce/aggregate step.
1502
+ */
1503
+ async executeReduceStep(step, context, _sdkRegistry, _stepExecutor) {
1504
+ const startedAt = new Date();
1505
+ try {
1506
+ // Resolve items array
1507
+ const items = resolveTemplates(step.items, context);
1508
+ if (!Array.isArray(items)) {
1509
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, 'Items must be an array');
1510
+ }
1511
+ // Reduce items using the expression
1512
+ let accumulator = step.initialValue ?? null;
1513
+ for (const item of items) {
1514
+ context.variables[step.itemVariable] = item;
1515
+ context.variables[step.accumulatorVariable] = accumulator;
1516
+ accumulator = resolveTemplates(step.expression, context);
1517
+ delete context.variables[step.itemVariable];
1518
+ delete context.variables[step.accumulatorVariable];
1519
+ }
1520
+ return createStepResult(step.id, StepStatus.COMPLETED, accumulator, startedAt);
1521
+ }
1522
+ catch (error) {
1523
+ delete context.variables[step.itemVariable];
1524
+ delete context.variables[step.accumulatorVariable];
1525
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1526
+ }
1527
+ }
1528
+ /**
1529
+ * Execute parallel branches.
1530
+ */
1531
+ async executeParallelStep(step, context, sdkRegistry, stepExecutor) {
1532
+ const startedAt = new Date();
1533
+ try {
1534
+ // Execute branches in parallel
1535
+ const branchPromises = step.branches.map(async (branch) => {
1536
+ // Clone context for isolation
1537
+ const branchContext = this.cloneContext(context);
1538
+ // Execute branch steps
1539
+ const branchResults = [];
1540
+ for (const branchStep of branch.steps) {
1541
+ const result = await this.executeStep(branchStep, branchContext, sdkRegistry, stepExecutor);
1542
+ if (result.status === StepStatus.COMPLETED && branchStep.outputVariable) {
1543
+ branchContext.variables[branchStep.outputVariable] = result.output;
1544
+ branchResults.push(result.output);
1545
+ }
1546
+ if (result.status === StepStatus.FAILED) {
1547
+ throw new Error(`Branch ${branch.id} failed: ${errorToString(result.error)}`);
1548
+ }
1549
+ }
1550
+ return { branchId: branch.id, context: branchContext, results: branchResults };
1551
+ });
1552
+ // Wait for all branches (or limited concurrency)
1553
+ const branchResults = step.maxConcurrent
1554
+ ? await this.executeConcurrentlyWithLimit(branchPromises, step.maxConcurrent)
1555
+ : await Promise.all(branchPromises);
1556
+ // Merge branch contexts back into main context
1557
+ for (const { branchId, context: branchContext } of branchResults) {
1558
+ this.mergeContexts(context, branchContext, branchId);
1559
+ }
1560
+ const outputs = branchResults.map((br) => br.results);
1561
+ return createStepResult(step.id, StepStatus.COMPLETED, outputs, startedAt);
1562
+ }
1563
+ catch (error) {
1564
+ if (step.onError === 'continue') {
1565
+ return createStepResult(step.id, StepStatus.COMPLETED, null, startedAt);
1566
+ }
1567
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1568
+ }
1569
+ }
1570
+ /**
1571
+ * Execute try/catch/finally step.
1572
+ */
1573
+ async executeTryStep(step, context, sdkRegistry, stepExecutor) {
1574
+ const startedAt = new Date();
1575
+ let tryError;
1576
+ try {
1577
+ // Execute try block
1578
+ for (const tryStep of step.try) {
1579
+ const result = await this.executeStep(tryStep, context, sdkRegistry, stepExecutor);
1580
+ if (result.status === StepStatus.COMPLETED && tryStep.outputVariable) {
1581
+ context.variables[tryStep.outputVariable] = result.output;
1582
+ }
1583
+ if (result.status === StepStatus.FAILED) {
1584
+ tryError = new Error(result.error ? errorToString(result.error) : 'Step failed');
1585
+ break;
1586
+ }
1587
+ }
1588
+ // If error occurred and catch block exists, execute catch
1589
+ let catchError;
1590
+ if (tryError && step.catch) {
1591
+ // Inject error object into context
1592
+ context.variables['error'] = {
1593
+ message: tryError.message,
1594
+ step: tryError,
1595
+ };
1596
+ for (const catchStep of step.catch) {
1597
+ const result = await this.executeStep(catchStep, context, sdkRegistry, stepExecutor);
1598
+ if (result.status === StepStatus.COMPLETED && catchStep.outputVariable) {
1599
+ context.variables[catchStep.outputVariable] = result.output;
1600
+ }
1601
+ if (result.status === StepStatus.FAILED) {
1602
+ catchError = new Error(result.error ? errorToString(result.error) : 'Catch block failed');
1603
+ break;
1604
+ }
1605
+ }
1606
+ delete context.variables['error'];
1607
+ }
1608
+ // Execute finally block (always runs)
1609
+ if (step.finally) {
1610
+ for (const finallyStep of step.finally) {
1611
+ const result = await this.executeStep(finallyStep, context, sdkRegistry, stepExecutor);
1612
+ if (result.status === StepStatus.COMPLETED && finallyStep.outputVariable) {
1613
+ context.variables[finallyStep.outputVariable] = result.output;
1614
+ }
1615
+ }
1616
+ }
1617
+ // Return success if catch handled the error, or error if not
1618
+ if (tryError && !step.catch) {
1619
+ // No catch block to handle error
1620
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, tryError.message);
1621
+ }
1622
+ if (catchError) {
1623
+ // Catch block also failed
1624
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, catchError.message);
1625
+ }
1626
+ return createStepResult(step.id, StepStatus.COMPLETED, null, startedAt);
1627
+ }
1628
+ catch (error) {
1629
+ // Execute finally even on unexpected error
1630
+ if (step.finally) {
1631
+ try {
1632
+ for (const finallyStep of step.finally) {
1633
+ const result = await this.executeStep(finallyStep, context, sdkRegistry, stepExecutor);
1634
+ if (result.status === StepStatus.COMPLETED && finallyStep.outputVariable) {
1635
+ context.variables[finallyStep.outputVariable] = result.output;
1636
+ }
1637
+ }
1638
+ }
1639
+ catch {
1640
+ // Ignore finally errors
1641
+ }
1642
+ }
1643
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1644
+ }
1645
+ }
1646
+ /**
1647
+ * Execute a script step (inline JavaScript).
1648
+ */
1649
+ async executeScriptStep(step, context) {
1650
+ const startedAt = new Date();
1651
+ try {
1652
+ // Resolve any templates in the code
1653
+ const resolvedInputs = resolveTemplates(step.inputs, context);
1654
+ // Execute the script with the workflow context
1655
+ const result = await executeScriptAsync(resolvedInputs.code, {
1656
+ variables: context.variables,
1657
+ inputs: context.inputs,
1658
+ steps: context.stepMetadata,
1659
+ }, {
1660
+ timeout: resolvedInputs.timeout ?? 5000,
1661
+ });
1662
+ if (!result.success) {
1663
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error ?? 'Script execution failed');
1664
+ }
1665
+ return createStepResult(step.id, StepStatus.COMPLETED, result.value, startedAt);
1666
+ }
1667
+ catch (error) {
1668
+ return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
1669
+ }
1670
+ }
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
+ // ============================================================================
1880
+ // Helper Methods for Control Flow
1881
+ // ============================================================================
1882
+ /**
1883
+ * Clone execution context for parallel branches.
1884
+ */
1885
+ cloneContext(context) {
1886
+ return {
1887
+ ...context,
1888
+ variables: { ...context.variables },
1889
+ inputs: { ...context.inputs },
1890
+ stepMetadata: { ...context.stepMetadata },
1891
+ };
1892
+ }
1893
+ /**
1894
+ * Merge branch context back into main context.
1895
+ */
1896
+ mergeContexts(mainContext, branchContext, branchId) {
1897
+ // Merge variables with branch prefix
1898
+ for (const [key, value] of Object.entries(branchContext.variables)) {
1899
+ mainContext.variables[`${branchId}.${key}`] = value;
1900
+ }
1901
+ }
1902
+ /**
1903
+ * Execute promises with concurrency limit.
1904
+ */
1905
+ async executeConcurrentlyWithLimit(promises, limit) {
1906
+ const results = [];
1907
+ const executing = [];
1908
+ for (const promise of promises) {
1909
+ const p = promise.then((result) => {
1910
+ results.push(result);
1911
+ });
1912
+ executing.push(p);
1913
+ if (executing.length >= limit) {
1914
+ await Promise.race(executing);
1915
+ executing.splice(executing.findIndex((x) => x === p), 1);
1916
+ }
1917
+ }
1918
+ await Promise.all(executing);
1919
+ return results;
1920
+ }
585
1921
  }
586
1922
  // ============================================================================
587
1923
  // Helpers