Filament Approval
Approval Workflow Engine for Filament
#Filament Approval Workflow Engine
A configurable approval workflow package for Filament. Attach approval chains to any Eloquent model with single, sequential, or parallel approvers, SLA timers, escalation rules, delegation, and a full audit trail.
#Requirements
- Filament 4+
#Features
- Polymorphic design -- attach approvals to any model via a trait
- Single, sequential, and parallel approval chains
- Configurable approval flows with a Filament resource UI
- Approve, reject, comment, and delegate actions
- SLA timers with auto-escalation (notify, auto-approve, reassign, reject)
- Delegation -- approvers can delegate to another user
- Full audit trail of every action
- Pluggable approver resolvers (specific users, roles, custom callbacks)
- Filament database notifications (requested, approved, rejected, escalated, SLA warning)
- Dashboard widgets (pending approvals table with clickable rows, analytics stats)
- Approval status badge column for resource tables
- Approvals relation manager (full history with slide-over detail view)
- Infolist section for current approval status at a glance
- Per-panel plugin configuration (user model, resolvers, navigation group)
- Scheduled command for SLA processing
- Publishable config and views
#Installation
composer require wezlo/filament-approval
Publish and run migrations:
php artisan vendor:publish --tag=filament-approval-migrations
php artisan migrate
Ensure you have a notifications table (required for Filament database notifications):
php artisan make:notifications-table php artisan migrate
Register the plugin in your Panel Provider:
use Wezlo\FilamentApproval\FilamentApprovalPlugin; ->plugins([ FilamentApprovalPlugin::make(), ])
You can override resolvers, user model, and navigation group per-panel:
use Wezlo\FilamentApproval\FilamentApprovalPlugin; // SuperAdmin panel -- uses Admin model and custom resolvers ->plugins([ FilamentApprovalPlugin::make() ->userModel(\App\Models\Admin::class) ->resolvers([ \App\ApproverResolvers\AdminResolver::class, ]) ->navigationGroup('Admin Approvals'), ]) // Company panel -- uses defaults from config ->plugins([ FilamentApprovalPlugin::make(), ])
Resolution order: plugin override (per-panel) > config file (global) > default fallback.
You can also disable the flow resource or widgets per-panel:
FilamentApprovalPlugin::make() ->flowResource(false) // hide the Approval Flows resource ->widgets(false) // hide dashboard widgets
Publish the config (optional):
php artisan vendor:publish --tag=filament-approval-config
#Quick Start
#1. Add the trait to your model
use Wezlo\FilamentApproval\Concerns\HasApprovals; class PurchaseOrder extends Model { use HasApprovals; }
This gives you:
$order->submitForApproval(); // Submit using auto-detected flow $order->submitForApproval($flow); // Submit using a specific flow $order->isPendingApproval(); // Check if pending $order->isApproved(); // Check if approved $order->isRejected(); // Check if rejected $order->approvalStatus(); // Get ApprovalStatus enum $order->latestApproval(); // Get latest Approval model $order->currentApproval(); // Get current pending Approval $order->approvals; // All approval instances
#2. Add approval actions to your resource page
use Wezlo\FilamentApproval\Concerns\HasApprovalsResource; class ViewPurchaseOrder extends ViewRecord { use HasApprovalsResource; protected static string $resource = PurchaseOrderResource::class; protected function getHeaderActions(): array { return [ ...$this->getApprovalHeaderActions(), // your other actions... ]; } }
This adds five context-aware actions to the page header:
- Submit for Approval -- visible when no pending approval
- Approve -- visible to assigned/delegated approvers who haven't acted
- Reject -- same visibility, requires a comment
- Comment -- visible to assigned approvers during a pending approval
- Delegate -- visible to assigned approvers, lets them delegate to another user
#3. Add the status column to your table
use Wezlo\FilamentApproval\Columns\ApprovalStatusColumn; public static function table(Table $table): Table { return $table->columns([ TextColumn::make('title'), ApprovalStatusColumn::make(), // ... ]); }
Displays a colored badge: Pending (warning), Approved (success), Rejected (danger), Cancelled (gray).
#4. Add approval history to your resource
Relation Manager -- full approval history tab on View/Edit pages:
use Wezlo\FilamentApproval\RelationManagers\ApprovalsRelationManager; public static function getRelations(): array { return [ ApprovalsRelationManager::class, ]; }
This adds an "Approvals" tab showing all approval instances. Clicking "View" on any row opens a slide-over with:
- Approval details (flow, status, submitter, dates)
- Step progress (each step with status, approvers, received/required count, SLA)
- Full audit trail (every action with who, what, when, comment)
Infolist Section -- current approval at a glance on View pages:
use Wezlo\FilamentApproval\Infolists\ApprovalStatusSection; public static function infolist(Schema $schema): Schema { return $schema->components([ // your other entries... ApprovalStatusSection::make(), ]); }
Shows a collapsible section with:
- Current status badge, flow name, submitter, dates
- Current step details (name, pending approvers, progress, SLA deadline with overdue highlighting)
- Recent activity timeline (collapsible)
The section auto-hides when there's no approval on the record.
#5. Create an approval flow
Navigate to the Approvals > Approval Flows resource in your Filament panel. Create a flow with:
- Name -- e.g. "Purchase Order Approval"
- Applies To -- dropdown auto-populated with models that use
HasApprovalsand are registered as resources in the current panel. Leave blank to apply to any model. - Steps -- ordered list with:
- Step name
- Type: Single / Sequential / Parallel
- Approver type: dropdown of resolvers configured on the plugin (e.g. Specific Users, Users by Role, or your custom resolvers)
- Approver config: dynamic fields based on the selected resolver (user picker, role selector, etc.)
- Required approvals (visible for Parallel type, with "Require N of M" hint)
- SLA hours (optional)
- Escalation action (visible when SLA is set)
#Approval Flow Types
#Single
One approver, one approval required. The simplest flow.
#Sequential
Multiple steps executed in order. Step 2 doesn't activate until step 1 is approved. A rejection at any step rejects the entire approval and skips remaining steps.
#Parallel
Multiple approvers on a single step. Configure required_approvals to set how many must approve (e.g., 2-of-3). The step completes when the threshold is met.
#Approver Resolvers
Resolvers determine who can approve each step. Three are included:
#UserResolver
Assigns specific users by ID:
Approver Type: Specific Users Config: Select users from the dropdown
#RoleResolver
Assigns all users with a given Spatie role. When multi-tenancy is enabled, approvers are scoped to the approvable model's tenant:
Approver Type: Users by Role Config: Select a role
#CallbackResolver
Register named callbacks in your service provider for custom logic:
use Wezlo\FilamentApproval\ApproverResolvers\CallbackResolver; // In AppServiceProvider::boot() CallbackResolver::register('project_manager', function ($approvable) { return [$approvable->project->manager_user_id]; }); CallbackResolver::register('department_head', function ($approvable) { return [$approvable->department->head_user_id]; });
Then select "Custom Callback" as the approver type in the flow builder.
#Custom Resolvers
Implement the ApproverResolver contract:
use Wezlo\FilamentApproval\Contracts\ApproverResolver; use Illuminate\Database\Eloquent\Model; class TeamLeadResolver implements ApproverResolver { public function resolve(array $config, Model $approvable): array { return $approvable->team->leads->pluck('id')->all(); } public static function label(): string { return 'Team Leads'; } public static function configSchema(): array { return [ // Filament form components for configuring this resolver ]; } }
Register it in config/filament-approval.php:
'approver_resolvers' => [ \Wezlo\FilamentApproval\ApproverResolvers\UserResolver::class, \Wezlo\FilamentApproval\ApproverResolvers\RoleResolver::class, \Wezlo\FilamentApproval\ApproverResolvers\CallbackResolver::class, \App\ApproverResolvers\TeamLeadResolver::class, ],
#SLA & Escalation
Configure SLA on any step in the flow builder:
- SLA (hours) -- deadline after the step is activated
- Escalation action -- what happens when the SLA is breached:
- Send Reminder -- notifies approvers again
- Auto-Approve -- automatically approves the step
- Auto-Reject -- automatically rejects the entire approval
- Reassign -- reassigns to different approvers (configure via escalation config)
The approval:process-sla command runs every minute (configurable) and:
- Sends SLA warnings at 75% of the deadline (configurable threshold)
- Processes escalations when the deadline is breached
The command is auto-scheduled by the package. To disable:
// config/filament-approval.php 'schedule_sla_command' => false,
#Delegation
Any assigned approver can delegate their approval authority to another user. The delegate can then approve or reject on their behalf. Delegations are recorded in the audit trail.
#Submission Policy
By default, any authenticated user can submit any record for approval, and re-submission is allowed after approval or rejection. Override these methods on your model to customize:
#One-time approval (no re-submission)
class Contract extends Model { use HasApprovals; /** * Once approved or rejected, the submit button won't appear again. */ public function allowsApprovalResubmission(): bool { return false; } }
#Restrict who can submit
class PurchaseOrder extends Model { use HasApprovals; /** * Only the creator or admins can submit for approval. */ public function canSubmitForApproval(?int $userId = null): bool { $userId ??= auth()->id(); return $this->created_by === $userId || User::find($userId)?->hasRole('admin'); } }
#Combine both
class Invoice extends Model { use HasApprovals; public function allowsApprovalResubmission(): bool { // Allow resubmission only if previously rejected (not if approved) $latest = $this->latestApproval(); return ! $latest || $latest->status !== ApprovalStatus::Approved; } public function canSubmitForApproval(?int $userId = null): bool { return $this->created_by === ($userId ?? auth()->id()); } }
The canBeSubmittedForApproval() method combines all checks (pending status + resubmission policy + user authorization) and is used by the Submit action's visibility logic.
#Audit Trail
Every action is recorded in the approval_actions table:
- Submitted
- Approved (with optional comment)
- Rejected (with required comment)
- Commented
- Delegated (with target user and reason)
- Escalated (with escalation action taken)
Access the audit trail:
$approval = $order->latestApproval(); $actions = $approval->actions; // Collection of ApprovalAction models
#Notifications
The package sends Filament database notifications for:
- Approval Requested -- sent to each assigned approver when a step activates
- Approval Approved -- sent to the submitter when the full approval completes
- Approval Rejected -- sent to the submitter when rejected
- SLA Warning -- sent to approvers when approaching the deadline
- Escalated -- sent to approvers when the SLA is breached
#Dashboard Widgets
The plugin registers two widgets:
#PendingApprovalsWidget
A table showing the current user's pending approvals with step name, record reference, time waiting, and SLA status. Overdue items are highlighted in red.
#ApprovalAnalyticsWidget
Stats overview with:
- Pending approvals count
- Approved in last 30 days
- Rejected in last 30 days
- Overdue steps count
Disable widgets:
FilamentApprovalPlugin::make() ->widgets(false)
Disable the flow resource:
FilamentApprovalPlugin::make() ->flowResource(false)
#Blade Components
The package includes Blade components for custom views:
{{-- Approval timeline --}} <x-filament-approval::components.approval-timeline :actions="$approval->actions" /> {{-- Status badge --}} <x-filament-approval::components.approval-status-badge :status="$approval->status" /> {{-- Full approval history (pass the approvable record) --}} @include('filament-approval::infolists.approval-history', ['record' => $order])
Publish views:
php artisan vendor:publish --tag=filament-approval-views
#Events & Model Callbacks
There are two ways to react to approval lifecycle events: Laravel events (for decoupled listeners) and model callbacks (for logic that belongs on the model itself).
#Laravel Events
Listen to these events via event listeners or subscribers:
use Wezlo\FilamentApproval\Events\ApprovalSubmitted; use Wezlo\FilamentApproval\Events\ApprovalStepCompleted; use Wezlo\FilamentApproval\Events\ApprovalCompleted; use Wezlo\FilamentApproval\Events\ApprovalRejected; use Wezlo\FilamentApproval\Events\ApprovalEscalated;
Each event carries the relevant Approval or ApprovalStepInstance model.
#Model Lifecycle Callbacks
Override these methods on any model using HasApprovals to react directly on the model:
use Wezlo\FilamentApproval\Concerns\HasApprovals; use Wezlo\FilamentApproval\Models\Approval; use Wezlo\FilamentApproval\Models\ApprovalAction; use Wezlo\FilamentApproval\Models\ApprovalStepInstance; class PurchaseOrder extends Model { use HasApprovals; public function onApprovalSubmitted(Approval $approval): void { $this->update(['status' => 'pending_approval']); } public function onApprovalApproved(Approval $approval): void { $this->update(['status' => 'approved']); Mail::to($this->requester)->send(new OrderApprovedMail($this)); } public function onApprovalRejected(Approval $approval): void { $this->update(['status' => 'rejected']); } public function onApprovalCancelled(Approval $approval): void { $this->update(['status' => 'draft']); } public function onApprovalCommented(ApprovalAction $action): void { // Notify the team about the comment } public function onApprovalDelegated( ApprovalStepInstance $stepInstance, int $fromUserId, int $toUserId, ): void { // Log delegation } public function onApprovalStepCompleted(ApprovalStepInstance $stepInstance): void { // Notify when a step passes } public function onApprovalEscalated(ApprovalStepInstance $stepInstance): void { // Alert management about SLA breach } }
All callbacks are optional -- only override the ones you need. They are called after the action has been persisted to the database.
| Callback | When it fires | Arguments |
|---|---|---|
onApprovalSubmitted |
Model submitted for approval | Approval |
onApprovalApproved |
All steps approved | Approval |
onApprovalRejected |
Rejected at any step | Approval |
onApprovalCancelled |
Approval cancelled | Approval |
onApprovalCommented |
Comment added | ApprovalAction |
onApprovalDelegated |
Approver delegates | ApprovalStepInstance, $fromUserId, $toUserId |
onApprovalStepCompleted |
Individual step approved | ApprovalStepInstance |
onApprovalEscalated |
SLA breached | ApprovalStepInstance |
#Programmatic Usage
Use the ApprovalEngine service directly:
use Wezlo\FilamentApproval\Services\ApprovalEngine; $engine = app(ApprovalEngine::class); // Submit $approval = $engine->submit($order, $flow, auth()->id()); // Approve a step $engine->approve($stepInstance, $userId, 'Looks good'); // Reject $engine->reject($stepInstance, $userId, 'Budget exceeded'); // Comment $engine->comment($approval, $userId, 'Please review section 3'); // Delegate $engine->delegate($stepInstance, $fromUserId, $toUserId, 'On vacation'); // Cancel $engine->cancel($approval);
#Multi-Tenancy
Multi-tenancy is disabled by default. When enabled, approval flows are scoped per tenant and the tenant column is added to the approval_flows migration.
Enable it in config/filament-approval.php:
'multi_tenancy' => [ 'enabled' => true, 'column' => 'company_id', // or 'team_id', 'organization_id', etc. 'scope_approvers' => true, // scope role-based resolvers to the tenant ],
When scope_approvers is true, the RoleResolver will only return users that share the same tenant as the approvable model.
Important: Configure multi-tenancy before running migrations. The tenant column is only added to the
approval_flowstable whenmulti_tenancy.enabledistrue.
#Configuration
// config/filament-approval.php return [ 'user_model' => \App\Models\User::class, 'approver_resolvers' => [ \Wezlo\FilamentApproval\ApproverResolvers\UserResolver::class, \Wezlo\FilamentApproval\ApproverResolvers\RoleResolver::class, \Wezlo\FilamentApproval\ApproverResolvers\CallbackResolver::class, ], 'multi_tenancy' => [ 'enabled' => false, 'column' => 'company_id', 'scope_approvers' => true, ], 'sla_warning_threshold' => 0.75, // 75% of SLA time 'schedule_sla_command' => true, 'navigation_group' => 'Approvals', 'table_prefix' => '', ];
#Translations
The package ships with English and Arabic translations. All UI strings (labels, messages, notifications, enum values) are fully translated.
Publish translations to customize:
php artisan vendor:publish --tag=filament-approval-translations
This copies the language files to lang/vendor/filament-approval/. The translation file is organized by section:
status.*-- Approval statuses (Pending, Approved, Rejected, Cancelled)step_type.*-- Step types (Single, Sequential, Parallel)action_type.*-- Audit trail actions (Submitted, Approved, Delegated, etc.)escalation.*-- Escalation actions (Send Reminder, Auto-Approve, etc.)flow.*-- Flow builder form labelsactions.*-- Approval action buttons and modalsnotifications.*-- Database notification titles and bodieswidgets.*-- Dashboard widget labelsrelation_manager.*-- Relation manager labelsinfolist.*-- Infolist section labels
To add a new language, create lang/vendor/filament-approval/{locale}/approval.php with the same structure.
#Custom Theme
If you have a custom Filament theme, add the package views to your @source directive:
@source '../../../../vendor/wezlo/filament-approval/resources/views/**/*';
#Testing
php artisan test --filter=ApprovalEngine
#License
MIT