Skip to main content
The execution engine is the core of AutoMFlows, responsible for orchestrating workflow execution, managing browser automation, and tracking results.

Architecture Overview

The execution engine consists of several interconnected components:
1

Workflow Parser

Parses workflow JSON, builds dependency graph, validates structure
2

Executor

Main orchestrator that manages workflow execution lifecycle
3

Context Manager

Maintains execution state, variables, browser contexts, database connections
4

Playwright Manager

Handles browser lifecycle, page management, video recording
5

Node Handlers

Execute individual node operations
6

Execution Tracker

Collects execution data, generates reports, manages screenshots

Executor Class

The Executor is the main execution orchestrator:
// backend/src/engine/executor/core.ts:35
export class Executor {
  private executionId: string;
  private workflow: Workflow;
  private parser: WorkflowParser;
  private context: ContextManager;
  private playwright: PlaywrightManager;
  private io: Server;                           // WebSocket server
  private status: ExecutionStatus = ExecutionStatus.IDLE;
  private currentNodeId: string | null = null;
  private stopRequested: boolean = false;
  private traceLogs: boolean;
  private nodeTraceLogs: Map<string, string[]>;
  private screenshotConfig?: ScreenshotConfig;
  private snapshotConfig?: SnapshotConfig;
  private reportConfig?: ReportConfig;
  private executionTracker?: ExecutionTracker;
  private recordSession: boolean;
  private breakpointConfig?: BreakpointConfig;
  private builderModeEnabled: boolean = false;
  private slowMo: number = 0;
  
  // Pause state for debugging
  private isPaused: boolean = false;
  private pausedNodeId: string | null = null;
  private pauseReason: 'wait-pause' | 'breakpoint' | null = null;
  
  // Execution state tracking
  private executedNodeIds: Set<string> = new Set();
  private currentExecutionOrder: string[] = [];
}

Execution Lifecycle

The execution follows this flow:
// backend/src/engine/executor/core.ts:71
constructor(
  workflow: Workflow, 
  io: Server, 
  traceLogs: boolean = false,
  screenshotConfig?: ScreenshotConfig,
  reportConfig?: ReportConfig,
  recordSession: boolean = false,
  breakpointConfig?: BreakpointConfig,
  builderModeEnabled: boolean = false,
  workflowFileName?: string,
  playwrightManager?: PlaywrightManager
) {
  this.executionId = uuidv4();
  
  // Migrate old nodes to new format
  const migrationResult = migrateWorkflow(workflow);
  this.workflow = { ...workflow, nodes: migrationResult.nodes };
  
  // Initialize parser and context
  this.parser = new WorkflowParser(this.workflow);
  this.context = new ContextManager();
  
  // Extract Start node configuration
  const startNode = workflow.nodes.find(node => node.type === NodeType.START);
  if (startNode) {
    const startNodeData = startNode.data as StartNodeData;
    this.screenshotConfig = startNodeData.screenshotAllNodes ? { ... } : undefined;
    this.snapshotConfig = startNodeData.snapshotAllNodes ? { ... } : undefined;
    this.slowMo = startNodeData.slowMo || 0;
  }
  
  // Initialize execution tracker if needed
  if (screenshotConfig || snapshotConfig || reportConfig || recordSession) {
    this.executionTracker = new ExecutionTracker(...);
    this.playwright = new PlaywrightManager(
      this.executionTracker.getScreenshotsDirectory(),
      this.executionTracker.getVideosDirectory(),
      this.recordSession
    );
  }
}
// Validate workflow structure before execution
const validation = this.parser.validate();
if (!validation.valid) {
  throw new Error(`Workflow validation failed: ${validation.errors.join(', ')}`);
}

// Check for:
// - Missing Start node
// - Circular dependencies
// - Invalid reusable flow references
// - Multiple driver connections
// backend/src/engine/parser.ts:114
getExecutionOrder(excludeReusableScopes: boolean = true): string[] {
  const visited = new Set<string>();
  const result: string[] = [];
  
  const visit = (nodeId: string): void => {
    if (visited.has(nodeId)) return;
    
    const node = this.nodes.get(nodeId);
    
    // Visit dependencies first (topological sort)
    for (const depId of node.dependencies) {
      visit(depId);
    }
    
    visited.add(nodeId);
    result.push(nodeId);
    
    // Visit dependents
    for (const dependentId of node.dependents) {
      visit(dependentId);
    }
  };
  
  visit(this.startNodeId);
  return result;
}
// Simplified execution loop
async execute(): Promise<void> {
  this.status = ExecutionStatus.RUNNING;
  this.emitEvent({ type: ExecutionEventType.EXECUTION_START });
  
  const executionOrder = this.parser.getExecutionOrder();
  this.currentExecutionOrder = executionOrder;
  
  for (const nodeId of executionOrder) {
    if (this.stopRequested) break;
    
    this.currentNodeId = nodeId;
    const node = this.parser.getNode(nodeId);
    
    // Emit node start event
    this.emitEvent({ 
      type: ExecutionEventType.NODE_START, 
      nodeId,
      timestamp: Date.now() 
    });
    
    try {
      // Take pre-execution screenshot if configured
      if (this.screenshotConfig?.timing === 'pre' || this.screenshotConfig?.timing === 'both') {
        await takeNodeScreenshot(node, this.context, this.executionTracker, 'pre');
      }
      
      // Resolve property inputs
      const resolvedNode = await resolvePropertyInputs(node, this.context);
      
      // Get handler and execute
      const handler = getNodeHandler(node.type);
      if (!handler) {
        throw new Error(`No handler found for node type: ${node.type}`);
      }
      
      await handler.execute(resolvedNode, this.context);
      
      // Take post-execution screenshot if configured
      if (this.screenshotConfig?.timing === 'post' || this.screenshotConfig?.timing === 'both') {
        await takeNodeScreenshot(node, this.context, this.executionTracker, 'post');
      }
      
      // Emit node complete event
      this.emitEvent({ 
        type: ExecutionEventType.NODE_COMPLETE, 
        nodeId,
        timestamp: Date.now() 
      });
      
      this.executedNodeIds.add(nodeId);
      
      // Apply slowMo delay
      if (this.slowMo > 0) {
        await new Promise(resolve => setTimeout(resolve, this.slowMo));
      }
      
    } catch (error) {
      // Handle node error
      await this.handleNodeError(node, error);
      
      if (!node.data.failSilently) {
        throw error; // Stop execution
      }
      // Otherwise continue to next node
    }
  }
  
  this.status = ExecutionStatus.COMPLETED;
  this.emitEvent({ type: ExecutionEventType.EXECUTION_COMPLETE });
  
  // Generate reports
  if (this.reportConfig?.enabled) {
    await generateReports(
      this.executionTracker,
      this.reportConfig,
      this.workflow
    );
  }
}
async cleanup(): Promise<void> {
  // Close browser
  await this.playwright.close();
  
  // Close database connections
  await this.context.closeAllDbConnections();
  
  // Clear action recorder session if builder mode
  if (this.builderModeEnabled) {
    ActionRecorderSessionManager.getInstance().clearSession(this.executionId);
  }
}

Context Manager

The ContextManager maintains execution state throughout workflow execution:
// backend/src/engine/context.ts:4
export class ContextManager {
  private context: ExecutionContext;
  private dbConnections: Map<string, any>;
  private contexts: Map<string, BrowserContext>;  // Multiple browser contexts
  private currentContextKey: string | null;
  
  constructor() {
    this.context = {
      data: {},          // Shared data between nodes
      variables: {},     // Named variables
    };
  }
  
  // Page management
  setPage(page: any): void {
    this.context.page = page;
  }
  
  getPage(): any {
    // If we have a current context, get page from it
    if (this.currentContextKey) {
      const browserContext = this.contexts.get(this.currentContextKey);
      if (browserContext) {
        const pages = browserContext.pages();
        if (pages.length > 0) return pages[0];
      }
    }
    return this.context.page;
  }
  
  // Data management
  setData(key: string, value: any): void {
    this.context.data[key] = value;
  }
  
  getData(key: string): any {
    return this.context.data[key];
  }
  
  // Variable management
  setVariable(key: string, value: any): void {
    this.context.variables[key] = value;
  }
  
  // Database connection management
  setDbConnection(key: string, connection: any): void {
    this.dbConnections.set(key, connection);
  }
  
  getDbConnection(key: string): any {
    return this.dbConnections.get(key);
  }
  
  async closeAllDbConnections(): Promise<void> {
    for (const [key, connection] of this.dbConnections.entries()) {
      // Close PostgreSQL/MySQL pool
      if (connection && typeof connection.end === 'function') {
        await connection.end();
      }
      // Close MongoDB/SQLite
      else if (connection && typeof connection.close === 'function') {
        await connection.close();
      }
    }
    this.dbConnections.clear();
  }
  
  // Browser context management
  setContext(key: string, browserContext: BrowserContext): void {
    this.contexts.set(key, browserContext);
  }
  
  getBrowserContext(key?: string): BrowserContext | null {
    const contextKey = key || this.currentContextKey;
    if (!contextKey) return null;
    return this.contexts.get(contextKey) || null;
  }
  
  setCurrentContextKey(key: string): void {
    if (!this.contexts.has(key)) {
      throw new Error(`Context with key "${key}" does not exist`);
    }
    this.currentContextKey = key;
    // Update page reference
    const browserContext = this.contexts.get(key);
    if (browserContext) {
      const pages = browserContext.pages();
      if (pages.length > 0) {
        this.context.page = pages[0];
      }
    }
  }
}

Context Data Flow

// Nodes store their outputs in context.data
// Example: Element Query node
const text = await locator.textContent();
context.setData('elementText', text);

// Example: API Request node
const response = await axios.request(config);
context.setData('apiResponse', {
  status: response.status,
  headers: response.headers,
  body: response.data
});

Playwright Integration

The PlaywrightManager handles all browser automation:
// backend/src/utils/playwright.ts
export class PlaywrightManager {
  private browser: Browser | null = null;
  private browserContext: BrowserContext | null = null;
  private page: Page | null = null;
  private browserType: 'chromium' | 'firefox' | 'webkit' = 'chromium';
  private screenshotsDirectory?: string;
  private videosDirectory?: string;
  private recordSession: boolean;
  private logger?: Logger;
  
  async launch(
    headless: boolean,
    viewport?: { width: number; height: number },
    browserType: 'chromium' | 'firefox' | 'webkit' = 'chromium',
    maxWindow: boolean = true,
    capabilities: Record<string, any> = {},
    stealthMode: boolean = false,
    launchOptions: Record<string, any> = {},
    jsScript?: string
  ): Promise<Page> {
    this.browserType = browserType;
    
    // Get Playwright browser type
    let playwright: BrowserType;
    if (browserType === 'firefox') {
      playwright = firefox;
    } else if (browserType === 'webkit') {
      playwright = webkit;
    } else {
      playwright = chromium;
    }
    
    // Apply stealth mode if enabled
    if (stealthMode) {
      const stealth = require('puppeteer-extra-plugin-stealth')();
      playwright = require('playwright-extra')[browserType];
      playwright.use(stealth);
    }
    
    // Launch browser
    const options: any = {
      headless,
      ...launchOptions
    };
    
    this.browser = await playwright.launch(options);
    
    // Create browser context with capabilities
    const contextOptions: any = {
      viewport: viewport || undefined,
      ...capabilities
    };
    
    // Enable video recording if configured
    if (this.recordSession && this.videosDirectory) {
      contextOptions.recordVideo = {
        dir: this.videosDirectory,
        size: viewport || { width: 1280, height: 720 }
      };
    }
    
    this.browserContext = await this.browser.newContext(contextOptions);
    
    // Inject JavaScript if provided
    if (jsScript) {
      await this.browserContext.addInitScript(jsScript);
    }
    
    // Setup console log capture
    if (this.logger) {
      this.browserContext.on('page', (page) => {
        page.on('console', (msg) => {
          this.logger?.logBrowserConsole(msg.type(), msg.text());
        });
      });
    }
    
    // Create page
    this.page = await this.browserContext.newPage();
    
    // Maximize window if requested
    if (maxWindow && !viewport) {
      const session = await this.page.context().newCDPSession(this.page);
      const { windowId } = await session.send('Browser.getWindowForTarget');
      await session.send('Browser.setWindowBounds', {
        windowId,
        bounds: { windowState: 'maximized' }
      });
    }
    
    return this.page;
  }
  
  async close(): Promise<void> {
    if (this.page) await this.page.close();
    if (this.browserContext) await this.browserContext.close();
    if (this.browser) await this.browser.close();
  }
  
  getBrowser(): Browser | null {
    return this.browser;
  }
  
  getBrowserContext(): BrowserContext | null {
    return this.browserContext;
  }
  
  getPage(): Page | null {
    return this.page;
  }
}

Browser Context Management

AutoMFlows supports multiple browser contexts for isolation:
1

Default Context

Created when Open Browser node executes
2

Named Contexts

Create additional contexts using Context Manipulate node with createContext action
3

Context Switching

Switch between contexts using switchContext action or Navigation node’s contextKey
4

Isolated Sessions

Each context has separate cookies, storage, and permissions

Retry and Wait Strategies

The execution engine provides sophisticated retry and wait mechanisms:

Wait Helper

// backend/src/utils/waitHelper.ts
export class WaitHelper {
  static async executeWaits(
    page: Page,
    config: {
      waitForSelector?: string;
      waitForUrl?: string;
      waitForCondition?: string;
      waitStrategy?: 'sequential' | 'parallel';
      waitTiming?: 'before' | 'after';
      defaultTimeout?: number;
      failSilently?: boolean;
    },
    context: ContextManager
  ): Promise<void> {
    const waits: Promise<any>[] = [];
    
    // Wait for selector
    if (config.waitForSelector) {
      const wait = page.waitForSelector(
        config.waitForSelector,
        { timeout: config.waitForSelectorTimeout || config.defaultTimeout }
      );
      waits.push(wait);
    }
    
    // Wait for URL pattern
    if (config.waitForUrl) {
      const wait = page.waitForURL(
        new RegExp(config.waitForUrl),
        { timeout: config.waitForUrlTimeout || config.defaultTimeout }
      );
      waits.push(wait);
    }
    
    // Wait for JavaScript condition
    if (config.waitForCondition) {
      const condition = VariableInterpolator.interpolateString(
        config.waitForCondition,
        context
      );
      const wait = page.waitForFunction(
        condition,
        { timeout: config.waitForConditionTimeout || config.defaultTimeout }
      );
      waits.push(wait);
    }
    
    // Execute waits based on strategy
    if (config.waitStrategy === 'parallel') {
      await Promise.all(waits);
    } else {
      // Sequential execution
      for (const wait of waits) {
        await wait;
      }
    }
  }
}

Retry Helper

// backend/src/utils/retryHelper.ts
export class RetryHelper {
  static async executeWithRetry<T>(
    operation: () => Promise<T>,
    retryConfig: {
      retryEnabled?: boolean;
      retryStrategy?: 'count' | 'untilCondition';
      retryCount?: number;
      retryDelay?: number;
      retryDelayStrategy?: 'fixed' | 'exponential';
      retryMaxDelay?: number;
      retryUntilCondition?: any;
    },
    context: ContextManager
  ): Promise<T> {
    if (!retryConfig.retryEnabled) {
      return await operation();
    }
    
    let attempt = 0;
    const maxAttempts = retryConfig.retryCount || 3;
    
    while (attempt < maxAttempts) {
      try {
        const result = await operation();
        
        // Check retry condition if strategy is 'untilCondition'
        if (retryConfig.retryStrategy === 'untilCondition') {
          const conditionMet = await this.evaluateRetryCondition(
            retryConfig.retryUntilCondition,
            context
          );
          
          if (conditionMet) {
            return result;
          }
          
          // Condition not met, retry
          attempt++;
          if (attempt < maxAttempts) {
            await this.delay(attempt, retryConfig);
            continue;
          }
        }
        
        return result;
        
      } catch (error) {
        attempt++;
        if (attempt >= maxAttempts) {
          throw error;
        }
        
        await this.delay(attempt, retryConfig);
      }
    }
    
    throw new Error('Max retry attempts reached');
  }
  
  private static async delay(
    attempt: number,
    config: any
  ): Promise<void> {
    const baseDelay = config.retryDelay || 1000;
    let delay: number;
    
    if (config.retryDelayStrategy === 'exponential') {
      delay = Math.min(
        baseDelay * Math.pow(2, attempt - 1),
        config.retryMaxDelay || 30000
      );
    } else {
      delay = baseDelay;
    }
    
    await new Promise(resolve => setTimeout(resolve, delay));
  }
}

Real-time Updates

The executor sends WebSocket events for real-time UI updates:
// WebSocket event emission
private emitEvent(event: ExecutionEvent): void {
  this.io.to(`execution-${this.executionId}`).emit('execution-event', event);
}

// Event types
enum ExecutionEventType {
  EXECUTION_START = 'execution_start',
  NODE_START = 'node_start',
  NODE_COMPLETE = 'node_complete',
  NODE_ERROR = 'node_error',
  EXECUTION_COMPLETE = 'execution_complete',
  EXECUTION_ERROR = 'execution_error',
  EXECUTION_PAUSED = 'execution_paused',
  BREAKPOINT_TRIGGERED = 'breakpoint_triggered',
  BUILDER_MODE_READY = 'builder_mode_ready',
  LOG = 'log'
}
Frontend listens to these events and updates the canvas in real-time, highlighting active nodes.

Report Generation

After execution, the ExecutionTracker generates reports:
// backend/src/engine/executor/reporting.ts
export async function generateReports(
  tracker: ExecutionTracker,
  config: ReportConfig,
  workflow: Workflow
): Promise<void> {
  const metadata = tracker.getMetadata();
  
  for (const reportType of config.reportTypes) {
    switch (reportType) {
      case 'html':
        await ReportGenerator.generateHtmlReport(metadata, tracker.getOutputDirectory());
        break;
      case 'allure':
        await ReportGenerator.generateAllureReport(metadata, tracker.getOutputDirectory());
        break;
      case 'json':
        await ReportGenerator.generateJsonReport(metadata, tracker.getOutputDirectory());
        break;
      case 'junit':
        await ReportGenerator.generateJunitReport(metadata, tracker.getOutputDirectory());
        break;
      case 'csv':
        await ReportGenerator.generateCsvReport(metadata, tracker.getOutputDirectory());
        break;
      case 'markdown':
        await ReportGenerator.generateMarkdownReport(metadata, tracker.getOutputDirectory());
        break;
    }
  }
  
  // Enforce retention policy
  if (config.reportRetention) {
    await enforceReportRetention(
      config.outputPath || './output',
      config.reportRetention
    );
  }
}

Performance Optimizations

Parallel Property Resolution

Property input connections are resolved in parallel when possible

Lazy Screenshot Capture

Screenshots only captured when configured or on error

Connection Pooling

Database connections reused across nodes

Context Reuse

Browser contexts maintained for multiple page operations

Build docs developers (and LLMs) love