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:
- Exception is caught
- Execution log is marked as failed
- Error message and stack trace are stored
- 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:
- Execution is marked as failed
- Error node ID is recorded
WorkflowFailedevent is fired- 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:
- Execution status updates in real-time
- Node progress indicator shows current step
- Logs appear as each node completes
Scheduled Execution
How Scheduling Works
-
Scheduler Command runs every minute:
php artisan workflows:run-scheduled -
Finds due workflows:
Workflow::where('status', 'active') ->where('trigger_type', 'schedule') ->get() ->filter(fn($w) => $this->isDue($w)); -
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
- API Reference - Execution API endpoints
- Extending Workflows - Custom node execution
- Examples - Real workflow examples