Workflow Execution Engine

The Execution Engine is the heart of the Workflows module. It handles the actual running of workflows, managing state, error handling, and detailed logging.

Execution Overview

┌───────────────────────────────────────────────────────────────────────┐
│                         EXECUTION ENGINE                               │
├───────────────────────────────────────────────────────────────────────┤
│                                                                        │
│   Trigger                                                              │
│      │                                                                 │
│      ▼                                                                 │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │  1. Create WorkflowExecution record (status: pending)        │   │
│   │  2. Initialize execution context                              │   │
│   │  3. Mark as running                                           │   │
│   └──────────────────────────────────────────────────────────────┘   │
│      │                                                                 │
│      ▼                                                                 │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │  4. Find entry point nodes (is_entry_point = true)           │   │
│   │  5. For each entry node:                                      │   │
│   │     └─► executeNode(node, triggerData)                       │   │
│   └──────────────────────────────────────────────────────────────┘   │
│      │                                                                 │
│      ▼                                                                 │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │  executeNode(node, input):                                    │   │
│   │    ├── Create WorkflowExecutionLog (status: pending)         │   │
│   │    ├── Get node instance from NodeRegistry                    │   │
│   │    ├── Execute: node->execute(input, config, context)        │   │
│   │    ├── Update log (status: completed, output data)           │   │
│   │    ├── Get outgoing connections                               │   │
│   │    └── For each connection:                                   │   │
│   │        ├── Check condition_path match                         │   │
│   │        └── Recursive: executeNode(nextNode, output)          │   │
│   └──────────────────────────────────────────────────────────────┘   │
│      │                                                                 │
│      ▼                                                                 │
│   ┌──────────────────────────────────────────────────────────────┐   │
│   │  6. Mark execution as completed/failed                        │   │
│   │  7. Store final result                                        │   │
│   │  8. Fire completion events                                    │   │
│   └──────────────────────────────────────────────────────────────┘   │
│                                                                        │
└───────────────────────────────────────────────────────────────────────┘

Execution Lifecycle

1. Triggering Execution

Workflows can be triggered in four ways:

Trigger Type How It Starts
Manual User clicks Run button or calls POST /api/v1/workflows/{id}/execute
Schedule Cron scheduler matches current time
Webhook HTTP POST to POST /api/webhooks/workflows/{token}
Event System event fires (e.g., order created)

2. Creating Execution Record

When execution starts, a WorkflowExecution record is created:

WorkflowExecution::create([
    'workflow_id' => $workflow->id,
    'status' => 'pending',
    'trigger_type' => 'manual', // or 'schedule', 'webhook', 'event'
    'trigger_data' => $triggerData,
    'triggered_by' => $userId,
    'context' => [],
    'nodes_total' => $workflow->nodes()->count(),
]);

3. Execution Context

The execution context is a shared state available to all nodes:

$context = [
    'workflow_id' => 'uuid',
    'execution_id' => 'uuid',
    'trigger_data' => [ /* initial data */ ],
    'variables' => [
        // Set by SetVariable nodes
        'key' => 'value',
    ],
];

Nodes access context via the third parameter:

public function execute(array $input, array $config, array $context): array
{
    $workflowId = $context['workflow_id'];
    $myVar = $context['variables']['myVar'] ?? null;
    // ...
}

4. Node Execution

Each node execution follows this pattern:

protected function executeNode(WorkflowNode $node, array $inputData): array
{
    // 1. Get node instance from registry
    $nodeInstance = $this->nodeRegistry->make($node->node_type);

    // 2. Create execution log
    $log = WorkflowExecutionLog::create([
        'execution_id' => $this->currentExecution->id,
        'node_id' => $node->id,
        'node_type' => $node->node_type,
        'status' => 'pending',
        'input_data' => $inputData,
    ]);

    // 3. Execute the node
    $log->markAsRunning();
    $outputData = $nodeInstance->execute(
        $inputData,
        $node->config ?? [],
        $this->executionContext
    );
    $log->markAsCompleted($outputData);

    // 4. Follow connections
    foreach ($node->outgoingConnections as $connection) {
        if ($this->shouldFollowConnection($connection, $outputData)) {
            $this->executeNode($connection->targetNode, $outputData);
        }
    }

    return $outputData;
}

5. Conditional Branching

When a node outputs a _branch field, only matching connections are followed:

// Condition node output
{
    "_branch": "true",  // or "false", "case1", etc.
    ...otherData
}

// Connection has condition_path
// Only follow if _branch matches condition_path
if ($connection->condition_path === $outputData['_branch']) {
    $this->executeNode($connection->targetNode, $outputData);
}

6. Completion

After all nodes execute, the execution is marked complete:

$execution->markAsCompleted($results);
// status: 'completed'
// result: final output data
// completed_at: timestamp

Execution States

Workflow Execution States

                    ┌─────────┐
                    │ pending │
                    └────┬────┘
                         │ start
                         ▼
                    ┌─────────┐
           ┌───────│ running │───────┐
           │       └────┬────┘       │
           │ error      │ success   │ cancel
           ▼            ▼            ▼
      ┌────────┐   ┌───────────┐  ┌───────────┐
      │ failed │   │ completed │  │ cancelled │
      └────────┘   └───────────┘  └───────────┘
Status Description
pending Created but not started
running Currently executing
completed All nodes finished successfully
failed An error occurred
cancelled Manually stopped

Node Execution Log States

Status Description
pending Node queued for execution
running Node currently executing
completed Node finished successfully
failed Node threw an error
skipped Node skipped (condition not met)

Error Handling

Node-Level Errors

When a node fails:

  1. Exception is caught
  2. Execution log is marked as failed
  3. Error message and stack trace are stored
  4. Retry logic is checked
try {
    $outputData = $nodeInstance->execute($input, $config, $context);
    $log->markAsCompleted($outputData);
} catch (\Exception $e) {
    $log->markAsFailed($e->getMessage(), $e->getTraceAsString());

    // Check retry configuration
    if ($log->retry_attempt < $node->retry_count) {
        sleep($node->retry_delay_seconds);
        $log->incrementRetry();
        return $this->executeNode($node, $inputData); // Retry
    }

    throw $e; // Propagate if retries exhausted
}

Retry Configuration

Nodes can be configured to retry on failure:

Setting Description
retry_count Number of retry attempts (default: 0)
retry_delay_seconds Delay between retries (default: 0)
timeout_seconds Max execution time (default: 0 = unlimited)

Workflow-Level Errors

When any node fails and retries are exhausted:

  1. Execution is marked as failed
  2. Error node ID is recorded
  3. WorkflowFailed event is fired
  4. Error message is stored

Execution Logs

Structure

Each node execution creates a log entry:

workflow_execution_logs
├── id: uuid
├── execution_id: uuid (FK)
├── node_id: uuid (FK)
├── node_type: string
├── node_label: string
├── status: enum
├── input_data: json
├── output_data: json (nullable)
├── error_message: text (nullable)
├── error_trace: text (nullable)
├── retry_attempt: integer
├── started_at: timestamp
├── completed_at: timestamp (nullable)
├── duration_ms: integer (nullable)
└── timestamps

Querying Logs

// Get all logs for an execution
$logs = WorkflowExecutionLog::where('execution_id', $executionId)
    ->orderBy('started_at')
    ->get();

// Get failed nodes
$failedLogs = WorkflowExecutionLog::where('execution_id', $executionId)
    ->where('status', 'failed')
    ->get();

// Get execution timeline
$timeline = WorkflowExecutionLog::where('execution_id', $executionId)
    ->select('node_label', 'status', 'started_at', 'duration_ms')
    ->orderBy('started_at')
    ->get();

Monitoring Executions

Viewing Execution History

Navigate to Workflows > [workflow] > Executions to see:

  • List of all executions with status
  • Trigger type and time
  • Duration and node count
  • Quick filters (status, date range)

Execution Detail View

Click an execution to see:

  • Summary: Status, duration, trigger info
  • Timeline: Visual progression through nodes
  • Logs: Per-node input/output data
  • Errors: Error messages with stack traces

Real-time Monitoring

For long-running workflows:

  1. Execution status updates in real-time
  2. Node progress indicator shows current step
  3. Logs appear as each node completes

Scheduled Execution

How Scheduling Works

  1. Scheduler Command runs every minute:

    php artisan workflows:run-scheduled
    
  2. Finds due workflows:

    Workflow::where('status', 'active')
        ->where('trigger_type', 'schedule')
        ->get()
        ->filter(fn($w) => $this->isDue($w));
    
  3. Dispatches jobs:

    foreach ($dueWorkflows as $workflow) {
        ExecuteWorkflowJob::dispatch($workflow, [], 'schedule');
    }
    

Cron Integration

Add to Laravel scheduler in app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->command('workflows:run-scheduled')
        ->everyMinute()
        ->withoutOverlapping()
        ->runInBackground();
}

Or the module automatically registers this when booted.

Webhook Execution

Webhook URL

Each workflow with webhook trigger gets a unique URL:

POST https://your-domain.com/api/webhooks/workflows/{workflow-token}

Signature Verification

For security, webhooks can require signature verification:

// Webhook node configuration
{
    "requireSignature": true,
    "signatureHeader": "X-Webhook-Signature",
    "signatureSecret": "your-secret-key"
}

// Verification
$expectedSignature = hash_hmac('sha256', $payload, $secret);
$providedSignature = $request->header('X-Webhook-Signature');

if (!hash_equals($expectedSignature, $providedSignature)) {
    abort(401, 'Invalid signature');
}

Webhook Response

Webhooks return immediately with execution ID:

{
  "success": true,
  "message": "Workflow execution started",
  "data": {
    "execution_id": "uuid",
    "status": "pending"
  }
}

Queue-Based Execution

Async Execution

For long-running workflows, use queue jobs:

// Dispatch to queue
ExecuteWorkflowJob::dispatch($workflow, $triggerData, 'manual', $userId);

// Job configuration
class ExecuteWorkflowJob implements ShouldQueue
{
    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 3600; // 1 hour max

    public function handle(ExecutionEngine $engine)
    {
        $engine->execute(
            $this->workflow,
            $this->triggerData,
            $this->triggerType,
            $this->triggeredBy
        );
    }
}

Queue Configuration

In your .env:

QUEUE_CONNECTION=redis

Run the queue worker:

php artisan queue:work --queue=workflows

Events

Available Events

Event When Fired Payload
WorkflowStarted Execution begins $execution
WorkflowCompleted Execution succeeds $execution
WorkflowFailed Execution fails $execution, $error

Listening to Events

// In EventServiceProvider
protected $listen = [
    \Modules\Workflows\App\Events\WorkflowCompleted::class => [
        \App\Listeners\NotifyOnWorkflowComplete::class,
    ],
];

// Listener
class NotifyOnWorkflowComplete
{
    public function handle(WorkflowCompleted $event)
    {
        $execution = $event->execution;
        // Send notification, update stats, etc.
    }
}

Performance Considerations

Large Workflows

For workflows with many nodes:

  • Use queue-based execution
  • Consider breaking into smaller workflows
  • Monitor memory usage

High-Volume Triggers

For frequently-triggered workflows:

  • Use dedicated queue workers
  • Consider rate limiting
  • Monitor queue depth

Long-Running Nodes

For nodes that take time (HTTP requests, delays):

  • Set appropriate timeouts
  • Use async patterns where possible
  • Monitor execution duration

Debugging

Enable Debug Logging

// In config/workflows.php
'debug' => env('WORKFLOWS_DEBUG', false),

With debug enabled:

  • All node inputs/outputs are logged
  • Execution timing is recorded
  • Stack traces are preserved

Common Issues

Issue Cause Solution
Workflow not executing Not activated Set status to active
Node timeout External service slow Increase timeout_seconds
Missing data Expression path wrong Check {{expression}} paths
Infinite loop Circular connections Review workflow design

Next Steps