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

Something wrong? Open an issue on GitHub