Define Workflows in Code
In DBFlow alpha, code-defined means your application builds a JSON-compatible definition array in PHP — then stores it in dbflow_workflow_versions through providers, sync actions, or Filament draft/publish flows.
There is no fluent PHP DSL such as ApprovalNode::make()->role(). The runtime source of truth is the JSON schema (schema_version: 1.0).
Visual authoring in DBFlow Pro (dbflowlabs/filament-pro) compiles canvas graphs into the same JSON structures through WorkflowGraphJsonParser and ProGraphBlueprintCompiler. Pro is Early Access / Preview — it edits definitions; Core still executes them. See Pro Visual Builder.
Why code-first arrays
When a workflow definition is produced from PHP arrays:
- It is versioned alongside your application in Git (provider classes, seeders, tests)
- It is reviewable in pull requests
- It is testable with PHPUnit and
WorkflowDefinitionValidator - It does not depend on a running admin UI to exist
Definition shape
Every definition includes:
| Field | Description |
|---|---|
key |
Unique workflow identifier (e.g. refund_dispute_approval) |
name |
Human-readable title |
schema_version |
Currently 1.0 |
enabled |
Optional; defaults true. Maps to dbflow_workflows.is_enabled on sync. |
nodes |
Array of node objects (start, approval, condition, action, end) |
transitions |
Array of edges between node keys |
Constants for field and node names live in DbflowLabs\Core\Definitions\WorkflowDefinitionSchema.
Node types
Start (type: start)
Entry point. Exactly one per workflow.
Approval (type: approval)
Suspends until an assignee approves or rejects.
[
'key' => 'support_lead_review',
'type' => 'approval',
'name' => 'Support Lead Review',
'config' => [
'approval_mode' => 'any', // any | all | sequential
'assignees' => [
'type' => 'user',
'value' => '2', // user ID as string
],
],
]
Assignee resolution
Approval assignees live under config.assignees. Constants: WorkflowDefinitionSchema::ASSIGNEE_TYPE_*.
user — fixed user ID
'assignees' => [
'type' => 'user',
'value' => '2', // single user ID as string
],
callback — host resolver key
'assignees' => [
'type' => 'callback',
'callback' => 'finance_team',
],
Register the resolver at boot:
DBFlow::registerAssigneeResolver(
app(AssigneeResolverRegistry::class),
'finance_team',
$financeTeamAssigneeResolver,
);
permission — resolver key alias (not ACL)
The permission type uses value as a registry key, identical in behaviour to callback. It does not integrate with Spatie Permission or Laravel policies automatically.
'assignees' => [
'type' => 'permission',
'value' => 'finance_team', // must match registerAssigneeResolver() key
],
role — schema only
role appears in WorkflowDefinitionSchema::assigneeTypes() for editors, but the open-core runtime throws Unsupported assignee configuration. Use callback + a resolver that reads your role system.
WorkflowDefinitionSchema::runtimeSupportedAssigneeTypes() returns user, permission, and callback.
Condition (type: condition)
Documentation/metadata node. Branching is controlled by transitions[].condition, evaluated with Symfony Expression Language against workflow variables from WorkflowContextInterface::getWorkflowVariables().
[
'key' => 'amount_gate',
'type' => 'condition',
'name' => 'Amount Gate',
'config' => [
'expression' => 'refund_amount >= 500', // optional display metadata
],
]
['from' => 'amount_gate', 'to' => 'risk_reviewer_review', 'condition' => 'refund_amount >= 500'],
['from' => 'amount_gate', 'to' => 'end_approved', 'is_default' => true],
Action (type: action)
Runs a registered handler key through Core's ActionManager (for example log, local_status_update, or Pro-registered premium handlers).
End (type: end)
Terminal node. config.status may be completed, approved, rejected, or cancelled.
WorkflowDefinitionProvider
Implement the provider contract to keep definitions in PHP classes:
<?php
namespace App\DBFlow\RefundDispute;
use DbflowLabs\Core\Contracts\WorkflowDefinitionProvider;
final class RefundDisputeWorkflowProvider implements WorkflowDefinitionProvider
{
public function key(): string
{
return 'refund_dispute_approval';
}
public function definition(): array
{
return [
'key' => $this->key(),
'name' => 'Refund Dispute Resolution Approval',
'schema_version' => '1.0',
'enabled' => true,
'nodes' => [/* ... */],
'transitions' => [/* ... */],
];
}
}
Register in a host service provider boot() method and sync into the database:
use DbflowLabs\Core\Actions\SyncWorkflowDefinitions;
use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Services\WorkflowDefinitionRegistry;
DBFlow::registerDefinitionProvider(
app(WorkflowDefinitionRegistry::class),
new RefundDisputeWorkflowProvider(),
);
app(SyncWorkflowDefinitions::class)->handle();
SyncWorkflowDefinitions validates each provider definition and upserts dbflow_workflows / dbflow_workflow_versions. It returns:
['created' => [...], 'updated' => [...], 'unchanged' => [...]]
Core does not ship an Artisan sync command — wrap this action in your deploy pipeline. See Host Integration.
Filament definition editor (code-synced workflows)
After sync, code-owned workflows have a published version but usually no draft. The Standard WorkflowResource node editor requires Workflow::hasDraft().
To expose the editor without recreating the workflow in UI, seed a draft from the published definition:
use DbflowLabs\Core\Actions\SaveWorkflowDraft;
use DbflowLabs\Core\Models\Workflow;
$workflow = Workflow::query()->where('key', $provider->key())->first();
if ($workflow && ! $workflow->hasDraft() && $workflow->hasPublishedVersion()) {
$definition = $workflow->currentDefinition();
$definition['key'] = $workflow->key;
$definition['name'] = $workflow->name;
app(SaveWorkflowDraft::class)->handle($workflow, $definition);
}
Run after each sync in boot, deploy, or a host Artisan command. Do not overwrite existing drafts or UI-owned workflows (source = ui). Details: Filament UI.
Alternative: draft and publish actions
The dbflow-demo project also builds definitions with helper factories and publishes them explicitly:
use DbflowLabs\Core\Actions\CreateWorkflowDraft;
use DbflowLabs\Core\Actions\PublishWorkflowDraft;
$workflow = app(CreateWorkflowDraft::class)->handle($definition, $actorUserId);
app(PublishWorkflowDraft::class)->handle($workflow, $actorUserId);
This is useful for seeders and one-off migrations of demo workflows.
Validation
Before publish or sync, validate topology and schema:
use DbflowLabs\Core\Validation\WorkflowDefinitionValidator;
app(WorkflowDefinitionValidator::class)->validateOrFail($definition);
Versioning
Published versions are stored per workflow in dbflow_workflow_versions. Active instances keep the version they started with. New starts resolve the currently published active version for that workflow key.
When you change a definition materially, publish a new version rather than mutating in-flight instance data.
Building nodes in PHP
DbflowLabs\Core\Support\WorkflowBuilderNodeFactory helps construct node arrays consistently (used heavily in dbflow-demo seeders).
What's next
- Host Integration → — full wiring checklist
- Conditions → — transition routing and variable design
- Refund Approval → — published definition example from
dbflow-demo - Purchase Request Approval → — procurement variant with hooks
- Approve and Reject → — task handling, reject strategies, audit logs
- Architecture → — concurrency model and DAG execution details