Skip to main content

Storage

The engine persists two things through separate repositories:

RepositoryContractHolds
WorkflowsWorkflowRepositoryContractWorkflow definitions.
ExecutionsExecutionRepositoryContractExecution records and their log rows.

Both are bound in the container — resolve them with app(...) or inject the contract. The concrete driver behind each is chosen by the storage config.

Workflow repository

use Qanna\WorkflowEngine\Storage\Contracts\WorkflowRepositoryContract;

$repo = app(WorkflowRepositoryContract::class);

$repo->all(); // Workflow[]
$repo->allActive();
$repo->find('send-welcome-email'); // ?Workflow
$repo->exists('send-welcome-email'); // bool
$repo->findByTriggerType('@wf::trigger.webhook');
$repo->create($workflow); // throws WorkflowAlreadyExistsException on dup id
$repo->update($workflow); // throws WorkflowNotFoundException if missing
$repo->upsert($workflow);
$repo->delete('send-welcome-email'); // bool

Execution repository

use Qanna\WorkflowEngine\Storage\Contracts\ExecutionRepositoryContract;
use Qanna\WorkflowEngine\Engine\Enums\ExecutionStatus;

$repo = app(ExecutionRepositoryContract::class);

$repo->find($executionId); // ?WorkflowExecution (logs included)
$repo->listForWorkflow('send-welcome-email', limit: 50, offset: 0);
$repo->countForWorkflow('send-welcome-email');
$repo->listByStatus(ExecutionStatus::Failed, limit: 50);
$repo->logsForExecution($executionId);
$repo->logsForNode($executionId, 'greet');
$repo->delete($executionId);
$repo->deleteForWorkflow('send-welcome-email');
$repo->purgeOlderThan(now()->subMonths(3)); // returns count deleted

The engine writes execution records itself as workflows run — you mostly read this repository (dashboards, cleanup jobs, debugging).

Drivers

DriverWorkflowsExecutions
fileJSON under storage/workflowsJSON under storage/workflow/executions
databaseworkflows tableworkflow_executions + workflow_execution_logs tables
memoryin-process array, nothing persists

Database drivers need the published migrations (vendor:publish --tag=workflowengine::migrations).

Sensitive fields

Node and trigger config fields declared ->sensitive() (for example the Webhook trigger's secret) are encrypted with the app key before being written and decrypted on read, transparently, by every driver. Rotating APP_KEY invalidates stored sensitive values.

Writing a custom driver

Register a factory on the StorageManager from a service provider's boot():

use Qanna\WorkflowEngine\Storage\StorageManager;

$this->app->make(StorageManager::class)->extend(
'redis',
fn (array $config, $container) => new RedisWorkflowRepository(/* ... */),
type: 'workflow', // or 'execution'
);

The factory receives the relevant storage.workflow / storage.execution config block and the container, and must return an implementation of the matching contract. Then set 'driver' => 'redis' in config.

For tests, StorageManager::forceDriver($repo, 'workflow' | 'execution') swaps in a specific instance without touching config. (Workflow::fake() uses this to force execution storage to memory — see Testing.)

Exceptions

Thrown by the repositories, all under Qanna\WorkflowEngine\Storage\Exceptions:

ExceptionWhen
WorkflowAlreadyExistsExceptioncreate() with an id that exists.
WorkflowNotFoundExceptionupdate() on a missing workflow.
ExecutionNotFoundExceptionupdate() on a missing execution.
StorageDriverException / StorageExceptionDriver-level failures.