@marktoflow/core 2.0.0-alpha.9 → 2.0.2

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 (149) hide show
  1. package/README.md +24 -222
  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 +92 -3
  15. package/dist/engine.d.ts.map +1 -1
  16. package/dist/engine.js +937 -59
  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 +382 -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 +1441 -54
  31. package/dist/models.d.ts.map +1 -1
  32. package/dist/models.js +124 -2
  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 +113 -3
  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/secrets/index.d.ts +12 -0
  98. package/dist/secrets/index.d.ts.map +1 -0
  99. package/dist/secrets/index.js +11 -0
  100. package/dist/secrets/index.js.map +1 -0
  101. package/dist/secrets/providers/aws.d.ts +32 -0
  102. package/dist/secrets/providers/aws.d.ts.map +1 -0
  103. package/dist/secrets/providers/aws.js +118 -0
  104. package/dist/secrets/providers/aws.js.map +1 -0
  105. package/dist/secrets/providers/azure.d.ts +40 -0
  106. package/dist/secrets/providers/azure.d.ts.map +1 -0
  107. package/dist/secrets/providers/azure.js +170 -0
  108. package/dist/secrets/providers/azure.js.map +1 -0
  109. package/dist/secrets/providers/env.d.ts +26 -0
  110. package/dist/secrets/providers/env.d.ts.map +1 -0
  111. package/dist/secrets/providers/env.js +59 -0
  112. package/dist/secrets/providers/env.js.map +1 -0
  113. package/dist/secrets/providers/vault.d.ts +39 -0
  114. package/dist/secrets/providers/vault.d.ts.map +1 -0
  115. package/dist/secrets/providers/vault.js +180 -0
  116. package/dist/secrets/providers/vault.js.map +1 -0
  117. package/dist/secrets/secret-manager.d.ts +72 -0
  118. package/dist/secrets/secret-manager.d.ts.map +1 -0
  119. package/dist/secrets/secret-manager.js +226 -0
  120. package/dist/secrets/secret-manager.js.map +1 -0
  121. package/dist/secrets/types.d.ts +105 -0
  122. package/dist/secrets/types.d.ts.map +1 -0
  123. package/dist/secrets/types.js +8 -0
  124. package/dist/secrets/types.js.map +1 -0
  125. package/dist/security.d.ts +1 -0
  126. package/dist/security.d.ts.map +1 -1
  127. package/dist/security.js +4 -0
  128. package/dist/security.js.map +1 -1
  129. package/dist/state.d.ts.map +1 -1
  130. package/dist/state.js +16 -9
  131. package/dist/state.js.map +1 -1
  132. package/dist/template-engine.d.ts +51 -0
  133. package/dist/template-engine.d.ts.map +1 -0
  134. package/dist/template-engine.js +227 -0
  135. package/dist/template-engine.js.map +1 -0
  136. package/dist/templates.d.ts +10 -0
  137. package/dist/templates.d.ts.map +1 -1
  138. package/dist/templates.js +21 -17
  139. package/dist/templates.js.map +1 -1
  140. package/dist/tools/mcp-tool.js +9 -9
  141. package/dist/tools/mcp-tool.js.map +1 -1
  142. package/dist/trigger-manager.js +1 -1
  143. package/dist/trigger-manager.js.map +1 -1
  144. package/dist/workflow-tools.d.ts +102 -0
  145. package/dist/workflow-tools.d.ts.map +1 -0
  146. package/dist/workflow-tools.js +130 -0
  147. package/dist/workflow-tools.js.map +1 -0
  148. package/package.json +31 -13
  149. package/LICENSE +0 -201
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, isActionStep, isSubWorkflowStep, isIfStep, isSwitchStep, isForEachStep, isWhileStep, isMapStep, isFilterStep, isReduceStep, isParallelStep, isTryStep, } 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,26 +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
- // Check if the entire string is a single template expression
100
- const singleTemplateMatch = value.match(/^\{\{([^}]+)\}\}$/);
101
- if (singleTemplateMatch) {
102
- // Return the actual value without converting to string
103
- const path = singleTemplateMatch[1].trim();
104
- const resolved = resolveVariablePath(path, context);
105
- // For single template expressions, return the actual value (could be object, array, etc.)
106
- // If undefined, return empty string for backward compatibility
107
- return resolved !== undefined ? resolved : '';
108
- }
109
- // Otherwise, do string interpolation
110
- return value.replace(/\{\{([^}]+)\}\}/g, (_, varPath) => {
111
- const path = varPath.trim();
112
- const resolved = resolveVariablePath(path, context);
113
- return resolved !== undefined ? String(resolved) : '';
114
- });
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);
115
176
  }
116
177
  if (Array.isArray(value)) {
117
178
  return value.map((v) => resolveTemplates(v, context));
@@ -141,6 +202,11 @@ export function resolveVariablePath(path, context) {
141
202
  if (fromVars !== undefined) {
142
203
  return fromVars;
143
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
+ }
144
210
  // Check step metadata (for status checks like: step_id.status)
145
211
  const fromStepMeta = getNestedValue(context.stepMetadata, path);
146
212
  if (fromStepMeta !== undefined) {
@@ -150,26 +216,63 @@ export function resolveVariablePath(path, context) {
150
216
  return getNestedValue(context, path);
151
217
  }
152
218
  /**
153
- * 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"
154
221
  */
155
222
  function getNestedValue(obj, path) {
156
223
  if (obj === null || obj === undefined) {
157
224
  return undefined;
158
225
  }
159
- const parts = path.split('.');
160
- 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;
161
259
  for (const part of parts) {
162
- if (current === null || current === undefined) {
260
+ if (result === null || result === undefined) {
163
261
  return undefined;
164
262
  }
165
- if (typeof current === 'object') {
166
- 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];
167
270
  }
168
271
  else {
169
272
  return undefined;
170
273
  }
171
274
  }
172
- return current;
275
+ return result;
173
276
  }
174
277
  export class WorkflowEngine {
175
278
  config;
@@ -181,13 +284,17 @@ export class WorkflowEngine {
181
284
  failoverConfig;
182
285
  healthTracker;
183
286
  failoverEvents = [];
184
- 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
185
290
  constructor(config = {}, events = {}, stateStore) {
186
291
  this.config = {
187
292
  defaultTimeout: config.defaultTimeout ?? 60000,
188
293
  maxRetries: config.maxRetries ?? 3,
189
294
  retryBaseDelay: config.retryBaseDelay ?? 1000,
190
295
  retryMaxDelay: config.retryMaxDelay ?? 30000,
296
+ defaultAgent: config.defaultAgent,
297
+ defaultModel: config.defaultModel,
191
298
  };
192
299
  this.retryPolicy = new RetryPolicy(this.config.maxRetries, this.config.retryBaseDelay, this.config.retryMaxDelay);
193
300
  this.events = events;
@@ -232,6 +339,15 @@ export class WorkflowEngine {
232
339
  if (isTryStep(step)) {
233
340
  return this.executeTryStep(step, context, sdkRegistry, stepExecutor);
234
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
+ }
235
351
  // Default: action or workflow step
236
352
  return this.executeStepWithFailover(step, context, sdkRegistry, stepExecutor);
237
353
  }
@@ -242,13 +358,22 @@ export class WorkflowEngine {
242
358
  const context = createExecutionContext(workflow, inputs);
243
359
  const stepResults = [];
244
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
+ }
245
370
  context.status = WorkflowStatus.RUNNING;
246
371
  this.events.onWorkflowStart?.(workflow, context);
247
372
  if (this.stateStore) {
248
373
  this.stateStore.createExecution({
249
374
  runId: context.runId,
250
375
  workflowId: workflow.metadata.id,
251
- workflowPath: 'unknown',
376
+ workflowPath: this.workflowPath ?? 'unknown',
252
377
  status: WorkflowStatus.RUNNING,
253
378
  startedAt: startedAt,
254
379
  completedAt: null,
@@ -272,12 +397,21 @@ export class WorkflowEngine {
272
397
  context.stepMetadata[step.id] = {
273
398
  status: result.status.toLowerCase(),
274
399
  retryCount: result.retryCount,
275
- ...(result.error ? { error: result.error } : {}),
400
+ ...(result.error ? { error: errorToString(result.error) } : {}),
276
401
  };
277
402
  // Store output variable
278
403
  if (step.outputVariable && result.status === StepStatus.COMPLETED) {
279
404
  context.variables[step.outputVariable] = result.output;
280
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
+ }
281
415
  // Handle failure
282
416
  if (result.status === StepStatus.FAILED) {
283
417
  // Get error action from step if it has error handling
@@ -287,7 +421,7 @@ export class WorkflowEngine {
287
421
  }
288
422
  if (errorAction === 'stop') {
289
423
  context.status = WorkflowStatus.FAILED;
290
- const workflowError = result.error || `Step ${step.id} failed`;
424
+ const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
291
425
  const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
292
426
  this.events.onWorkflowComplete?.(workflow, workflowResult);
293
427
  return workflowResult;
@@ -302,7 +436,7 @@ export class WorkflowEngine {
302
436
  });
303
437
  }
304
438
  context.status = WorkflowStatus.FAILED;
305
- const workflowError = result.error || `Step ${step.id} failed`;
439
+ const workflowError = result.error ? errorToString(result.error) : `Step ${step.id} failed`;
306
440
  const workflowResult = this.buildWorkflowResult(workflow, context, stepResults, startedAt, workflowError);
307
441
  this.events.onWorkflowComplete?.(workflow, workflowResult);
308
442
  return workflowResult;
@@ -336,6 +470,136 @@ export class WorkflowEngine {
336
470
  this.events.onWorkflowComplete?.(workflow, workflowResult);
337
471
  return workflowResult;
338
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
+ }
339
603
  /**
340
604
  * Execute a workflow from a file.
341
605
  * This method automatically sets the workflow path for resolving sub-workflows.
@@ -390,14 +654,302 @@ export class WorkflowEngine {
390
654
  // Return the sub-workflow output
391
655
  return result.output;
392
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
+ }
393
926
  /**
394
927
  * Execute a step with retry logic.
395
928
  */
396
929
  async executeStepWithRetry(step, context, sdkRegistry, stepExecutor) {
397
930
  const startedAt = new Date();
398
931
  let lastError;
932
+ // Build executor context with model/agent/permissions
933
+ const executorContext = this.buildStepExecutorContext(step);
399
934
  // Handle sub-workflow execution
400
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
401
953
  try {
402
954
  this.events.onStepStart?.(step, context);
403
955
  const output = await this.executeWithTimeout(() => this.executeSubWorkflow(step, context, sdkRegistry, stepExecutor), step.timeout ?? this.config.defaultTimeout);
@@ -407,7 +959,8 @@ export class WorkflowEngine {
407
959
  }
408
960
  catch (error) {
409
961
  lastError = error instanceof Error ? error : new Error(String(error));
410
- 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
+ );
411
964
  this.events.onStepComplete?.(step, result);
412
965
  return result;
413
966
  }
@@ -431,11 +984,30 @@ export class WorkflowEngine {
431
984
  }
432
985
  this.events.onStepStart?.(step, context);
433
986
  try {
434
- // Resolve templates in inputs
435
- 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
+ }
436
997
  const stepWithResolvedInputs = { ...step, inputs: resolvedInputs };
437
- // Execute step
438
- 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
+ }
439
1011
  circuitBreaker.recordSuccess();
440
1012
  const result = createStepResult(step.id, StepStatus.COMPLETED, output, startedAt, attempt);
441
1013
  this.events.onStepComplete?.(step, result);
@@ -453,7 +1025,8 @@ export class WorkflowEngine {
453
1025
  }
454
1026
  }
455
1027
  // All retries exhausted
456
- 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
+ );
457
1030
  this.events.onStepComplete?.(step, result);
458
1031
  return result;
459
1032
  }
@@ -472,7 +1045,7 @@ export class WorkflowEngine {
472
1045
  this.healthTracker.markHealthy(primaryTool);
473
1046
  return primaryResult;
474
1047
  }
475
- const errorMessage = primaryResult.error ?? '';
1048
+ const errorMessage = primaryResult.error ? errorToString(primaryResult.error) : '';
476
1049
  const isTimeout = errorMessage.includes('timed out');
477
1050
  if (isTimeout && !this.failoverConfig.failoverOnTimeout) {
478
1051
  this.healthTracker.markUnhealthy(primaryTool, errorMessage);
@@ -576,8 +1149,18 @@ export class WorkflowEngine {
576
1149
  /**
577
1150
  * Resolve a condition value with support for nested properties.
578
1151
  * Handles direct variable references and nested paths.
1152
+ * Uses Nunjucks for template expressions with filters/regex.
579
1153
  */
580
1154
  resolveConditionValue(path, context) {
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
+ }
581
1164
  // First try to parse as a literal value (true, false, numbers, etc.)
582
1165
  const parsedValue = this.parseValue(path);
583
1166
  // If parseValue returned the same string, try to resolve as a variable
@@ -615,12 +1198,14 @@ export class WorkflowEngine {
615
1198
  */
616
1199
  buildWorkflowResult(workflow, context, stepResults, startedAt, error) {
617
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;
618
1203
  return {
619
1204
  workflowId: workflow.metadata.id,
620
1205
  runId: context.runId,
621
1206
  status: context.status,
622
1207
  stepResults,
623
- output: context.variables,
1208
+ output,
624
1209
  error,
625
1210
  startedAt,
626
1211
  completedAt,
@@ -704,9 +1289,17 @@ export class WorkflowEngine {
704
1289
  }
705
1290
  /**
706
1291
  * Execute a for-each loop step.
1292
+ * Supports optional batch_size and pause_between_batches for rate limiting.
707
1293
  */
708
1294
  async executeForEachStep(step, context, sdkRegistry, stepExecutor) {
709
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
+ };
710
1303
  try {
711
1304
  // Resolve items array
712
1305
  const items = resolveTemplates(step.items, context);
@@ -716,10 +1309,15 @@ export class WorkflowEngine {
716
1309
  if (items.length === 0) {
717
1310
  return createStepResult(step.id, StepStatus.SKIPPED, [], startedAt);
718
1311
  }
719
- // 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
720
1319
  const results = [];
721
1320
  for (let i = 0; i < items.length; i++) {
722
- // Inject loop variables
723
1321
  context.variables[step.itemVariable] = items[i];
724
1322
  context.variables['loop'] = {
725
1323
  index: i,
@@ -739,35 +1337,82 @@ export class WorkflowEngine {
739
1337
  if (result.status === StepStatus.FAILED) {
740
1338
  const errorAction = step.errorHandling?.action ?? 'stop';
741
1339
  if (errorAction === 'stop') {
742
- // Clean up loop variables
743
- delete context.variables[step.itemVariable];
744
- delete context.variables['loop'];
745
- if (step.indexVariable)
746
- delete context.variables[step.indexVariable];
1340
+ cleanupLoopVars();
747
1341
  return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, result.error);
748
1342
  }
749
- // 'continue' - skip to next iteration
750
1343
  break;
751
1344
  }
752
1345
  }
753
1346
  results.push(context.variables[step.itemVariable]);
754
1347
  }
755
- // Clean up loop variables
756
- delete context.variables[step.itemVariable];
757
- delete context.variables['loop'];
758
- if (step.indexVariable)
759
- delete context.variables[step.indexVariable];
1348
+ cleanupLoopVars();
760
1349
  return createStepResult(step.id, StepStatus.COMPLETED, results, startedAt);
761
1350
  }
762
1351
  catch (error) {
763
- // Clean up loop variables on error
764
- delete context.variables[step.itemVariable];
765
- delete context.variables['loop'];
766
- if (step.indexVariable)
767
- delete context.variables[step.indexVariable];
1352
+ cleanupLoopVars();
768
1353
  return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
769
1354
  }
770
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
+ }
771
1416
  /**
772
1417
  * Execute a while loop step.
773
1418
  */
@@ -899,7 +1544,7 @@ export class WorkflowEngine {
899
1544
  branchResults.push(result.output);
900
1545
  }
901
1546
  if (result.status === StepStatus.FAILED) {
902
- throw new Error(`Branch ${branch.id} failed: ${result.error}`);
1547
+ throw new Error(`Branch ${branch.id} failed: ${errorToString(result.error)}`);
903
1548
  }
904
1549
  }
905
1550
  return { branchId: branch.id, context: branchContext, results: branchResults };
@@ -936,7 +1581,7 @@ export class WorkflowEngine {
936
1581
  context.variables[tryStep.outputVariable] = result.output;
937
1582
  }
938
1583
  if (result.status === StepStatus.FAILED) {
939
- tryError = new Error(result.error || 'Step failed');
1584
+ tryError = new Error(result.error ? errorToString(result.error) : 'Step failed');
940
1585
  break;
941
1586
  }
942
1587
  }
@@ -954,7 +1599,7 @@ export class WorkflowEngine {
954
1599
  context.variables[catchStep.outputVariable] = result.output;
955
1600
  }
956
1601
  if (result.status === StepStatus.FAILED) {
957
- catchError = new Error(result.error || 'Catch block failed');
1602
+ catchError = new Error(result.error ? errorToString(result.error) : 'Catch block failed');
958
1603
  break;
959
1604
  }
960
1605
  }
@@ -998,6 +1643,239 @@ export class WorkflowEngine {
998
1643
  return createStepResult(step.id, StepStatus.FAILED, null, startedAt, 0, error instanceof Error ? error.message : String(error));
999
1644
  }
1000
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
+ }
1001
1879
  // ============================================================================
1002
1880
  // Helper Methods for Control Flow
1003
1881
  // ============================================================================