Storage
The engine persists two things through separate repositories:
| Repository | Contract | Holds |
|---|---|---|
| Workflows | WorkflowRepositoryContract | Workflow definitions. |
| Executions | ExecutionRepositoryContract | Execution 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
| Driver | Workflows | Executions |
|---|---|---|
file | JSON under storage/workflows | JSON under storage/workflow/executions |
database | workflows table | workflow_executions + workflow_execution_logs tables |
memory | — | in-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:
| Exception | When |
|---|---|
WorkflowAlreadyExistsException | create() with an id that exists. |
WorkflowNotFoundException | update() on a missing workflow. |
ExecutionNotFoundException | update() on a missing execution. |
StorageDriverException / StorageException | Driver-level failures. |