Filament Resource Actions

Alpha notice: Patterns below reflect the current dbflow-demo integration. Your resource structure may differ.

Filament resource actions are the right place to start a workflow from business records. Approvals and rejections for assignees usually happen on My Tasks (MyWorkflowTasks), which already wraps DBFlow::approve() and DBFlow::reject().

Keep resource classes thin: extract lifecycle actions into a concern or dedicated action class, call Core through DbflowLabs\Core\DBFlow, and let hooks map workflow outcomes back to host status columns.

Pattern: HasLifecycleActions concern

The dbflow-demo project uses a trait per resource — for example RefundDisputeResource\Concerns\HasLifecycleActions and ProcurementRequestResource\Concerns\HasLifecycleActions.

Responsibilities split cleanly:

Method Role
getLifecycleHeaderActions() Register Filament Action instances
makeSubmit…Action() Visibility rules + DBFlow::start()
submitThroughDbflow() Runtime call + exception handling
makeLifecycleAction() Pure host mutations without DBFlow
runPureHostLifecycleMutation() Shared notification + update() helper

Start workflow from a resource

Submit actions call DBFlow::start() with the workflow key, workflowable model, and authenticated user:

use App\DBFlow\RefundDispute\RefundDisputeWorkflow;
use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Exceptions\WorkflowAlreadyRunningException;
use Illuminate\Support\Facades\Auth;

protected function submitThroughDbflow(RefundDispute $record): void
{
    try {
        DBFlow::start(
            RefundDisputeWorkflow::KEY,
            $record,
            Auth::user(),
        );

        Notification::make()->title('Submitted for review.')->success()->send();
    } catch (WorkflowAlreadyRunningException) {
        Notification::make()->title('A workflow is already running.')->danger()->send();
    }
}

Use named constants for workflow keys (RefundDisputeWorkflow::KEY) so resources and seeders stay aligned.

Visibility rules

Show submit only when no running workflow

->visible(fn (RefundDispute $record): bool =>
    $record->status === RefundDisputeStatus::Investigating
    && ! $record->hasRunningWorkflow(RefundDisputeWorkflow::KEY)
    && ! $record->isClosed()
)

hasRunningWorkflow($workflowKey) comes from the HasWorkflow trait.

Approve / reject on My Tasks, not the resource

Assignees act on WorkflowTaskAssignment rows through MyWorkflowTaskTableActions. Those actions call MyWorkflowTaskActionRunner, which invokes Core approve/reject handlers.

If you need approve/reject on a resource page, resolve the pending task explicitly:

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

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

if ($task !== null) {
    DBFlow::approve($task, auth()->user(), 'Approved from resource.');
}

Prefer My Tasks for routine approvals so permission checks stay centralized in WorkflowFilamentPermissions and your PermissionChecker implementation.

Implement WorkflowRouteResolvable on workflowable models so DBFlow instance/task tables can link back to your Filament edit page (getWorkflowShowUrl()).

Guard downstream business actions

Block conversion, payment, fulfillment, or archive actions until host status reflects approval:

->visible(fn (ProcurementRequest $record): bool =>
    $record->status === ProcurementRequestStatus::Approved
)

In the demo, archiveRecord is only visible when status is Approved. DBFlow does not own ERP or payment integration — those remain host actions gated by WorkflowHooks outcomes.

Map workflow outcomes with hooks

Register WorkflowHooks so terminal workflow states update your model columns:

// AppServiceProvider::boot()
DBFlow::registerWorkflowHooks(
    app(WorkflowHooksRegistry::class),
    ProcurementRequestWorkflow::KEY,
    ProcurementRequestWorkflowHooks::class,
);

ProcurementRequestWorkflowHooks::onApproved() sets status to approved. onRejected() sets rejected. onStarted() moves draft/rejected records to pending_review.

Keep ERP adapters, notifications, and inventory logic outside Core — trigger them from hooks or listeners you control.

Present workflow state on the resource

Demo resources use a presenter class (RefundDisputeWorkflowPresenter) to surface:

  • Running workflow status label
  • Pending task count
  • Link to ViewWorkflowInstance or My Tasks

This keeps infolist/table columns readable without bloating the resource class.

Dual-mode demo (DBFlow on/off)

dbflow-demo uses DbflowSettings::isEnabled() to fall back to pure host lifecycle buttons when DBFlow is disabled — useful for comparisons and tests. Production hosts typically omit the fallback and always route submissions through DBFlow.

Checklist

  1. Extract actions into a concern or action class.
  2. Start workflows with DBFlow::start($key, $model, $user).
  3. Guard submit with hasRunningWorkflow($key).
  4. Register WorkflowHooks for status mapping.
  5. Gate downstream mutations on host status, not on guessed workflow helpers.
  6. Link operators to My Tasks and instance detail for approvals and audit history.

What's next

Something wrong? Open an issue on GitHub