Build Your First Approval Workflow

Alpha notice: API names may change during alpha. The examples below match the current dbflowlabs/core runtime and the flows in our hosted demo.

This guide walks through a refund dispute approval workflow. A support lead reviews every case. Disputes with refund_amount >= 500 also require a risk reviewer before approval.

This mirrors the RefundDispute model and workflow key refund_dispute_approval in the live demo.

What you will build

Submit → Support Lead Review → [refund_amount >= 500] → Risk Review → Approved
                            ↘ [refund_amount < 500]  ↗
  • Two approval nodes (support_lead_review, risk_reviewer_review)
  • One condition gate (amount_gate) with transition conditions
  • Reject handled through DBFlow::reject() and RejectStrategy
  • Every action recorded in dbflow_workflow_logs

Step 1: Prepare a model

DBFlow does not require workflow columns on your business table. Workflow state lives in DBFlow tables.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class RefundDispute extends Model
{
    protected $guarded = [];

    protected function casts(): array
    {
        return ['refund_amount' => 'decimal:2'];
    }
}

Step 2: Attach DBFlow to the model

Add HasWorkflow and optional contracts for labels, variables, and Filament links:

<?php

namespace App\Models;

use DbflowLabs\Core\Contracts\Workflowable;
use DbflowLabs\Core\Contracts\WorkflowContextInterface;
use DbflowLabs\Core\Traits\HasWorkflow;
use Illuminate\Database\Eloquent\Model;

class RefundDispute extends Model implements Workflowable, WorkflowContextInterface
{
    use HasWorkflow;

    public function workflowBusinessKey(): ?string
    {
        return $this->reference_code;
    }

    public function workflowDisplayName(): string
    {
        return (string) $this->reference_code;
    }

    public function getWorkflowVariables(): array
    {
        return [
            'refund_amount' => (float) $this->refund_amount,
        ];
    }
}

Conditional routing reads variables from getWorkflowVariables() (via WorkflowContextInterface). Transitions evaluate expressions against those variables.

Step 3: Define and publish the workflow

In alpha, workflows are stored as JSON-compatible definition arrays in dbflow_workflow_versions, not as a PHP fluent node DSL.

A minimal provider pattern:

<?php

namespace App\DBFlow\RefundDispute;

use DbflowLabs\Core\Contracts\WorkflowDefinitionProvider;
use DbflowLabs\Core\Definitions\WorkflowDefinitionSchema;

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' => [
                ['key' => 'start', 'type' => 'start', 'name' => 'Start'],
                [
                    'key' => 'support_lead_review',
                    'type' => 'approval',
                    'name' => 'Support Lead Review',
                    'config' => [
                        'approval_mode' => 'any',
                        'assignees' => ['type' => 'user', 'value' => '2'],
                    ],
                ],
                [
                    'key' => 'amount_gate',
                    'type' => 'condition',
                    'name' => 'Amount Gate',
                    'config' => ['expression' => 'refund_amount >= 500'],
                ],
                [
                    'key' => 'risk_reviewer_review',
                    'type' => 'approval',
                    'name' => 'Risk Reviewer Review',
                    'config' => [
                        'approval_mode' => 'any',
                        'assignees' => ['type' => 'user', 'value' => '3'],
                    ],
                ],
                [
                    'key' => 'end_approved',
                    'type' => 'end',
                    'name' => 'Approved',
                    'config' => ['status' => 'approved'],
                ],
            ],
            'transitions' => [
                ['from' => 'start', 'to' => 'support_lead_review'],
                ['from' => 'support_lead_review', 'to' => 'amount_gate'],
                ['from' => 'amount_gate', 'to' => 'risk_reviewer_review', 'condition' => 'refund_amount >= 500'],
                ['from' => 'amount_gate', 'to' => 'end_approved', 'is_default' => true],
                ['from' => 'risk_reviewer_review', 'to' => 'end_approved'],
            ],
        ];
    }
}

Routing note: Branching is controlled by transitions[].condition. A condition node's config.expression is optional metadata; the runtime evaluates outgoing transition conditions.

Register the provider in a host service provider boot() method (not a route), then sync into the database (or publish via CreateWorkflowDraft + PublishWorkflowDraft as in dbflow-demo seeders):

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();

Required: DBFlow::start() fails until sync creates a published version. See Host Integration.

Step 4: Start the workflow

Trigger the workflow when a dispute is submitted — from a controller, Filament action, or job:

use DbflowLabs\Core\DBFlow;

$dispute = RefundDispute::create([
    'reference_code' => 'RD-10042',
    'refund_amount'  => 750.00,
]);

DBFlow::start('refund_dispute_approval', $dispute, $user, [
    'submit_comment' => 'Optional metadata stored on the instance',
]);

// Or via the HasWorkflow helper (uses DbflowAuth::currentUser()):
$dispute->startWorkflow('refund_dispute_approval');

DBFlow creates a dbflow_workflow_instances row, advances through non-blocking nodes, and creates the first pending WorkflowTask when an approval node is reached.

Step 5: Approve or reject a task

Approvals operate on a WorkflowTask model — not on a node key string helper.

use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Enums\RejectStrategy;
use DbflowLabs\Core\Enums\WorkflowTaskStatus;

$task = $dispute->runningWorkflowInstance('refund_dispute_approval')
    ?->tasks()
    ->where('status', WorkflowTaskStatus::Pending)
    ->first();

DBFlow::approve($task, $user, 'Receipt verified.');

DBFlow::reject($task, $user, 'Missing order number.', RejectStrategy::Starter);

In Filament, assignees typically act from the My Tasks page (MyWorkflowTasks), which calls the same runtime actions internally.

Step 6: Check workflow state

$instance = $dispute->runningWorkflowInstance('refund_dispute_approval');

$instance?->status;           // WorkflowInstanceStatus enum
$instance?->current_node_key; // e.g. 'risk_reviewer_review'
$dispute->hasRunningWorkflow('refund_dispute_approval'); // bool

Step 7: View the audit history

foreach ($dispute->workflowLogs('refund_dispute_approval')->get() as $log) {
    // $log->event      — WorkflowLogEvent value (e.g. task_approved)
    // $log->comment    — optional note
    // $log->actor      — user relation when present
    // $log->created_at
}

With DBFlow Filament installed, open Workflow Instances → detail page to see the timeline rendered by WorkflowInstanceTimelinePresenter.

Optional: lifecycle hooks

React to completion without Laravel event classes:

use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Services\WorkflowHooksRegistry;

DBFlow::registerWorkflowHooks(
    app(WorkflowHooksRegistry::class),
    'refund_dispute_approval',
    RefundDisputeWorkflowHooks::class,
);

Implement onStarted, onApproved, onRejected, and onCancelled on your WorkflowHooks class.

What's next

Something wrong? Open an issue on GitHub