Approve and Reject Tasks

Approval nodes suspend a workflow until an authorized actor approves or rejects. This page covers pending tasks, the runtime API, reject strategies, and audit logging.

Pending tasks

When the runtime reaches an approval node, DBFlow creates a WorkflowTask in dbflow_workflow_tasks and one or more rows in dbflow_workflow_task_assignments. The instance stays in running status until the task is acted on.

use DbflowLabs\Core\Enums\WorkflowTaskAssignmentStatus;
use DbflowLabs\Core\Enums\WorkflowTaskStatus;
use DbflowLabs\Core\Models\WorkflowTaskAssignment;

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

$pendingTask = $instance?->tasks()
    ->where('status', WorkflowTaskStatus::Pending)
    ->first();

$myAssignments = WorkflowTaskAssignment::query()
    ->where('assignee_user_id', $user->getKey())
    ->where('status', WorkflowTaskAssignmentStatus::Pending)
    ->get();

Assignees are defined in the workflow JSON under each approval node's config.assignees. See Assignee resolution and Host Integration.

type Alpha runtime
user Resolves a single user ID from value.
callback callback (or value) must match DBFlow::registerAssigneeResolver() key.
permission value is a resolver registry key — not Spatie Permission or Laravel Gate.
role Schema only — unsupported at runtime (throws InvalidWorkflowDefinitionException).

Approve a task

Pass the WorkflowTask model to the static runtime entrypoint:

use DbflowLabs\Core\DBFlow;

DBFlow::approve($task, $user, 'Looks good, approved.');

Security: When $actor is null, Core skips pending-assignment validation on approve. Always pass the authenticated user in production.

When approved, DBFlow:

  1. Writes a task_approved entry to dbflow_workflow_logs
  2. Marks the task and relevant assignments complete
  3. Advances the workflow to the next node, or completes the instance
  4. Clears active_key on the instance when it reaches a terminal state

Reject a task

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

DBFlow::reject($task, $user, 'Missing invoice attachment.', RejectStrategy::Starter);

The fourth argument is the reject strategy. The fifth argument ($targetNodeKey) is only used with RejectStrategy::SpecificNode.

Reject strategies

Enum case Stored value Behaviour
RejectStrategy::Starter starter Return toward the initiator / restart path
RejectStrategy::PreviousNode previous_node Return to the immediately preceding approval node
RejectStrategy::SpecificNode specific_node Return to a named node (pass $targetNodeKey)
RejectStrategy::End end Terminate the workflow as rejected
DBFlow::reject($task, $user, 'Escalation required.', RejectStrategy::PreviousNode);

DBFlow::reject(
    $task,
    $user,
    'Send back to support lead.',
    RejectStrategy::SpecificNode,
    'support_lead_review',
);

DBFlow::reject($task, $user, 'Rejected permanently.', RejectStrategy::End);

The Standard Filament My Tasks page defaults to RejectStrategy::End unless you override config('dbflow-filament.reject_strategy').

See Reject Strategies for a focused guide on when to use each enum case.

Approval modes

Multi-assignee approval nodes support three modes via approval_mode in the node config:

Enum case Value Behaviour
ApprovalMode::Any any Any one assignee can approve
ApprovalMode::All all All assignees must approve
ApprovalMode::Sequential sequential Assignees approve in sequence order

Comments

Comments are optional on approve and recommended on reject. They are stored on dbflow_workflow_logs.comment.

In the Filament UI, comments appear in the timeline next to the actor name and timestamp.

Audit logs

Every approve and reject action is recorded automatically through WorkflowLogger. Query logs via the model helper or directly:

foreach ($dispute->workflowLogs('refund_dispute_approval')->with('actor')->get() as $log) {
    echo "{$log->created_at} — {$log->event->value}";
}

WorkflowLogEvent classifies log rows (workflow_started, task_approved, task_rejected, workflow_completed, and others). These are log event types, not Laravel Event classes dispatched to the container.

Logged fields include:

  • Event type (WorkflowLogEvent)
  • Actor user (when available)
  • Related task (when applicable)
  • Comment (when provided)
  • JSON payload metadata
  • Timestamp

Double-submit protection

DBFlow stores a unique active_key on dbflow_workflow_instances while an instance is running. Format: {workflowKey}:{morphClass}:{workflowableId}.

If a second start is attempted for the same workflowable slot, the runtime throws WorkflowAlreadyRunningException. If a task is no longer pending, callers may receive TaskNotPendingException or UserCannotApproveTaskException. Starting against a missing or unpublished definition throws WorkflowNotFoundException, WorkflowNotPublishedException, or WorkflowNotAvailableException.

use DbflowLabs\Core\DBFlow;
use DbflowLabs\Core\Exceptions\TaskNotPendingException;
use DbflowLabs\Core\Exceptions\WorkflowAlreadyRunningException;

try {
    DBFlow::start('refund_dispute_approval', $dispute, $user);
} catch (WorkflowAlreadyRunningException) {
    // Already in flight for this record + workflow key
}

try {
    DBFlow::approve($task, $user);
} catch (TaskNotPendingException) {
    // Another actor already completed this task
}

All approval actions run inside database transactions.

Cancel a running workflow

use DbflowLabs\Core\DBFlow;

DBFlow::cancel($instance, $user, 'Withdrawn by submitter.');

Core marks the instance cancelled, cancels pending tasks, clears active_key, and invokes WorkflowHooks::onCancelled(). It does not verify that $user is the original submitter — enforce withdraw/cancel policy in your host layer before calling cancel().

Whether business actions may continue after cancel (for example confirming a draft document) is also host policy, not enforced by Core.

What's next

Something wrong? Open an issue on GitHub