Skip to main content
Workflows in AutoMFlows are represented as directed acyclic graphs (DAGs) where nodes represent operations and edges define execution flow and data dependencies.

Workflow JSON Structure

Every workflow is stored as a JSON file with this structure:
{
  "nodes": [
    {
      "id": "start-1",
      "type": "start",
      "position": { "x": 100, "y": 200 },
      "data": {
        "type": "start",
        "label": "Start",
        "screenshotAllNodes": true,
        "screenshotTiming": "post",
        "recordSession": false,
        "slowMo": 0
      }
    },
    {
      "id": "openBrowser-1",
      "type": "openBrowser",
      "position": { "x": 350, "y": 200 },
      "data": {
        "type": "openBrowser",
        "label": "Open Browser",
        "headless": false,
        "viewportWidth": 1280,
        "viewportHeight": 720,
        "browser": "chromium"
      }
    }
  ],
  "edges": [
    {
      "id": "e1",
      "source": "start-1",
      "target": "openBrowser-1",
      "sourceHandle": "output",
      "targetHandle": "driver"
    }
  ],
  "groups": [],
  "metadata": {
    "name": "My Workflow",
    "description": "Login test workflow",
    "version": 1,
    "tags": ["login", "test"],
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T12:45:00Z"
  }
}

Nodes

Nodes are the building blocks of workflows. Each node represents a single operation.

Node Interface

// shared/src/types.ts:79
export interface BaseNode {
  id: string;                           // Unique identifier
  type: NodeType | string;              // Node type (built-in or plugin)
  position: { x: number; y: number };   // Canvas position
  data: NodeData | Record<string, any>; // Node-specific configuration
}

Node Properties

Each node type has specific properties in its data field:
export interface StartNodeData {
  label?: string;
  recordSession?: boolean;           // Enable video recording
  screenshotAllNodes?: boolean;      // Screenshot on all nodes
  screenshotTiming?: 'pre' | 'post' | 'both';
  snapshotAllNodes?: boolean;        // Accessibility snapshots
  snapshotTiming?: 'pre' | 'post' | 'both';
  slowMo?: number;                   // Delay between nodes (ms)
  scrollThenAction?: boolean;        // Smooth scroll before UI actions
}

Common Properties

Most interaction nodes share these common properties:
waitForSelector?: string;          // Wait for element to appear
waitForSelectorType?: SelectorType;
waitForSelectorModifiers?: SelectorModifiers;
waitForSelectorTimeout?: number;

waitForUrl?: string;               // Wait for URL pattern (supports regex)
waitForUrlTimeout?: number;

waitForCondition?: string;         // JavaScript expression to evaluate
waitForConditionTimeout?: number;

waitStrategy?: 'sequential' | 'parallel';  // Execute waits sequentially or in parallel
waitAfterOperation?: boolean;      // Wait before (false) or after (true) operation
retryEnabled?: boolean;
retryStrategy?: 'count' | 'untilCondition';
retryCount?: number | string;      // Number of retries or variable reference

// Retry until condition is met
retryUntilCondition?: {
  type: 'selector' | 'url' | 'javascript';
  value: string;
  selectorType?: SelectorType;
  visibility?: 'visible' | 'invisible';
  timeout?: number | string;
};

retryDelay?: number | string;      // Delay between retries (ms)
retryDelayStrategy?: 'fixed' | 'exponential';  // Fixed delay or exponential backoff
retryMaxDelay?: number | string;   // Maximum delay for exponential strategy
_inputConnections?: {
  [propertyName: string]: {        // e.g., "text", "url", "selector"
    sourceNodeId: string;          // Node providing the value
    sourceHandleId: string;        // Output handle from source node
  };
};
Property input connections allow nodes to receive values dynamically from other nodes’ outputs, enabling data flow and reusable components.

Edges

Edges define relationships between nodes. There are two types:

Control Flow Edges

Control flow edges determine execution order. When a node completes, execution flows to its connected nodes.
// shared/src/types.ts:1333
export interface Edge {
  id: string;
  source: string;              // Source node ID
  target: string;              // Target node ID
  sourceHandle?: string;       // Output port ("output", "case-1", "default")
  targetHandle?: string;       // Input port ("driver", "input")
}
Common control flow handles:
  • sourceHandle: "output"targetHandle: "driver": Standard flow
  • sourceHandle: "output"targetHandle: "input": Loop entry
  • sourceHandle: "case-1"targetHandle: "driver": Switch branch
  • sourceHandle: "default"targetHandle: "driver": Switch default

Property Input Edges

Property input edges provide dynamic values to node properties. They create one-way data dependencies without affecting control flow.
// Example: Connect Int Value node output to Type node's text property
{
  "id": "e2",
  "source": "intValue-1",        // Int Value node
  "target": "type-1",            // Type node
  "sourceHandle": "output",
  "targetHandle": "text-input"  // Property-specific handle
}
Property input handles:
  • text-input: Dynamic text value
  • selector-input: Dynamic selector
  • url-input: Dynamic URL
  • timeout-input: Dynamic timeout
  • (Any property can have an input handle)

Edge Semantics

1

Dependency Resolution

WorkflowParser builds a dependency graph where each node knows its dependencies and dependents
2

Topological Sort

Execution order is determined via topological sort, ensuring dependencies execute first
3

Parallel Execution

Nodes with satisfied dependencies can execute in parallel if they’re independent
4

Circular Detection

The parser detects circular dependencies and throws errors before execution

Groups

Groups provide visual organization on the canvas:
// shared/src/types.ts:1342
export interface Group {
  id: string;
  name: string;
  nodeIds: string[];                    // Nodes in this group
  position: { x: number; y: number };
  width: number;
  height: number;
  borderColor?: string;
}
Groups are visual-only and don’t affect execution order.

Metadata

Workflow metadata provides documentation and versioning:
// shared/src/types.ts:1353
export interface WorkflowMetadata {
  name?: string;
  description?: string;
  author?: string;
  version?: number;
  tags?: string[];                     // For categorization
  createdAt?: string;                  // ISO 8601 timestamp
  updatedAt?: string;
  automflowsVersion?: string;          // AutoMFlows version used
}

Variable Interpolation

Node properties support dynamic values using ${...} syntax:
// Access context data
"${data.username}"              // From context.data.username
"${data.config.apiKey}"         // Nested property
"${data.apiResponse.body.id}"   // API response data

Implementation

// backend/src/utils/variableInterpolator.ts
export class VariableInterpolator {
  static interpolateString(str: string, context: ContextManager): string {
    return str.replace(/\$\{([^}]+)\}/g, (match, path) => {
      // Resolve data.key.path from context
      const value = this.resolvePath(path, context);
      return value !== undefined ? String(value) : match;
    });
  }
  
  static resolvePath(path: string, context: ContextManager): any {
    if (path.startsWith('data.')) {
      // Access context.data
      const key = path.substring(5);
      return this.getNestedValue(context.getAllData(), key);
    } else if (path.startsWith('variables.')) {
      // Access context.variables
      const key = path.substring(10);
      return this.getNestedValue(context.getAllVariables(), key);
    }
    return undefined;
  }
}

Execution Semantics

Dependency Graph

// backend/src/engine/parser.ts:10
export interface ExecutionNode {
  node: BaseNode;
  dependencies: string[];    // Nodes that must execute first
  dependents: string[];      // Nodes that depend on this one
}
The parser builds this graph during workflow loading:
// backend/src/engine/parser.ts:39
private buildDependencyGraph(): void {
  for (const edge of this.edges) {
    const isPropertyInput = edge.targetHandle && 
      edge.targetHandle !== 'driver' && 
      edge.targetHandle !== 'input';
    
    if (isPropertyInput) {
      // One-way dependency: target depends on source
      targetNode.dependencies.push(edge.source);
    } else {
      // Control flow: bidirectional relationship
      targetNode.dependencies.push(edge.source);
      sourceNode.dependents.push(edge.target);
    }
  }
}

Execution Order

Topological sort determines execution order:
// backend/src/engine/parser.ts:114
getExecutionOrder(): 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
    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;
}

Special Workflow Patterns

Switch nodes create multiple execution paths based on conditions:
// Multiple source handles for different cases
{
  "source": "switch-1",
  "sourceHandle": "case-1",  // Condition 1 true
  "target": "action-a"
}
{
  "source": "switch-1",
  "sourceHandle": "case-2",  // Condition 2 true
  "target": "action-b"
}
{
  "source": "switch-1",
  "sourceHandle": "default",  // No conditions true
  "target": "action-c"
}
Loop nodes execute a subgraph multiple times:
// forEach loop over array
{
  "mode": "forEach",
  "arrayVariable": "products",  // Context contains data.products array
}

// doWhile loop with condition
{
  "mode": "doWhile",
  "condition": {
    "type": "ui-element",
    "selector": ".next-button",
    "elementCheck": "visible"
  },
  "maxIterations": 100
}
Loop body is defined by edges from the loop node back to itself via the “input” handle.
Reusable nodes define sub-workflows that can be invoked multiple times:
// Define reusable flow
{
  "type": "reusable.reusable",
  "data": {
    "contextName": "loginFlow"  // Unique identifier
  }
}

// Invoke reusable flow
{
  "type": "reusable.runReusable",
  "data": {
    "contextName": "loginFlow",   // Reference to defined flow
    "inputs": {                   // Pass parameters
      "username": "${data.user}",
      "password": "${data.pass}"
    }
  }
}

Validation

The parser validates workflows before execution:
// backend/src/engine/parser.ts:308
validate(): { valid: boolean; errors: string[]; warnings: string[] } {
  const errors: string[] = [];
  const warnings: string[] = [];
  
  // Check for start node
  if (!this.startNodeId) {
    errors.push('Workflow must contain a Start node');
  }
  
  // Check for circular dependencies
  try {
    this.getExecutionOrder();
  } catch (error) {
    errors.push(error.message);  // "Circular dependency detected"
  }
  
  // Validate reusable flows have unique context names
  // Validate Run Reusable nodes reference existing flows
  // Check for multiple driver connections to same node
  
  return { valid: errors.length === 0, errors, warnings };
}

Build docs developers (and LLMs) love