Skip to main content

Extension Points

Beyond custom nodes and custom triggers, the engine exposes two lower-level extension points: custom storage drivers, and lifecycle hooks.

Custom storage drivers

Workflow and execution storage are resolved through Qanna\WorkflowEngine\Storage\StorageManager, which supports registering additional driver factories beyond the built-in file/database/memory ones (see Configuration § Storage drivers):

use Qanna\WorkflowEngine\Storage\StorageManager;

public function boot(): void
{
$this->app->make(StorageManager::class)->extend(
'redis',
function (array $config, \Illuminate\Contracts\Container\Container $app) {
return new \App\Workflows\Storage\RedisWorkflowRepository(/* ... */);
},
'workflow', // or 'execution'
);
}

The factory closure receives the driver's own config block (config('workflowengine.storage.workflow'), for example) and the container, and must return an instance implementing WorkflowRepositoryContract (see Workflows § Storing and retrieving workflows) or ExecutionRepositoryContract (see Execution § Execution history), matching the type you registered it under. Once registered, select it the normal way:

// config/workflowengine.php
'storage' => [
'workflow' => ['driver' => 'redis'],
],

Lifecycle hooks

Qanna\WorkflowEngine\Engine\HookDispatcher (a container singleton) lets you react to workflow lifecycle events without modifying any node:

use Qanna\WorkflowEngine\Engine\HookDispatcher;

public function boot(): void
{
$this->app->make(HookDispatcher::class)->on(
'workflow.finished',
function (string $workflowId, string $executionId, $status, ?string $errorMessage, $execution) {
// e.g. notify on failure
},
);
}

on(string $event, callable $callback, int $priority = 100, bool $once = false) registers a listener (lower $priority runs earlier); once() is a shorthand for a listener that removes itself after firing; off() removes a specific listener; flush() clears all listeners for an event (or every event).

Available events

EventFires withWhen
workflow.starting$workflowIdRight before a fresh execution begins.
workflow.started$workflowId, $executionId, $triggerOutputAfter the trigger has fired and the execution record exists.
workflow.trigger-ignored$workflowId, $payload, $reasonThe trigger returned TriggerResult::ignore() — no execution was created.
workflow.resumed$workflowId, $executionIdA suspended execution is about to continue.
workflow.node-executing$workflowId, $executionId, $nodeIdRight before a node's handle() is called.
workflow.node-executed$workflowId, $executionId, $nodeId, NodeResult $resultRight after — inspect $result for success/failure/output.
workflow.node-retrying$workflowId, $executionId, $nodeId, array{attempt, max}A node is about to retry after a failure (see Nodes: Overview § Retries and timeouts).
workflow.node-slow$workflowId, $executionId, $nodeId, array{elapsed, timeout, message}A node exceeded its configured timeout.
workflow.finished$workflowId, $executionId, $status, $errorMessage, $executionThe execution reached a terminal or suspended state.

Returning false from a hook callback is treated as a cancellation signal for that dispatch — subsequent listeners for the same event still run, but no other part of the engine currently checks this return value, so it's only meaningful between your own listeners on the same event.

Next