Architecture Overview
The execution engine consists of several interconnected components: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:1. Initialization
1. Initialization
// 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
);
}
}
2. Validation
2. Validation
// 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
3. Execution Order Determination
3. Execution Order Determination
// 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;
}
4. Node Execution Loop
4. Node Execution Loop
// 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
);
}
}
5. Cleanup
5. Cleanup
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
- Node Outputs
- Variable Interpolation
- Loop Variables
// 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
});
// Nodes can reference context data using ${data.key}
const url = node.data.url; // "https://api.com/user/${data.userId}"
const interpolated = VariableInterpolator.interpolateString(url, context);
// Result: "https://api.com/user/123" (if data.userId = 123)
// Loop nodes set iteration variables
context.setData('loopIteration', index);
context.setData('loopItem', currentItem);
context.setData('loopArray', array);
// Accessible in subsequent nodes as ${data.loopItem}
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:Context Switching
Switch between contexts using
switchContext action or Navigation node’s contextKeyRetry 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'
}
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
