Build Your First Approval Workflow
Alpha notice: API names may change during alpha. The examples below match the current
dbflowlabs/coreruntime 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()andRejectStrategy - 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'sconfig.expressionis 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
- Host Integration → — production wiring checklist
- Refund Approval → — full
dbflow-demoreference implementation - Testing Workflows → — PHPUnit patterns for workflow behaviour
- Eloquent Models → — model-first design principles
- Code-defined Workflows → — JSON schema, providers, and sync
- Approve and Reject → — reject strategies and concurrency