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.
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
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
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:
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).
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:
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:
The section auto-hides when there's no approval on the record.
Navigate to the Approvals > Approval Flows resource in your Filament panel. Create a flow with:
HasApprovals and are registered as resources in the current panel. Leave blank to apply to any model.One approver, one approval required. The simplest flow.
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.
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.
Resolvers determine who can approve each step. Three are included:
Assigns specific users by ID:
Approver Type: Specific Users
Config: Select users from the dropdown
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
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.
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,
],
Configure SLA on any step in the flow builder:
The approval:process-sla command runs every minute (configurable) and:
The command is auto-scheduled by the package. To disable:
// config/filament-approval.php
'schedule_sla_command' => false,
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.
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:
class Contract extends Model
{
use HasApprovals;
/**
* Once approved or rejected, the submit button won't appear again.
*/
public function allowsApprovalResubmission(): bool
{
return false;
}
}
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');
}
}
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.
Every action is recorded in the approval_actions table:
Access the audit trail:
$approval = $order->latestApproval();
$actions = $approval->actions; // Collection of ApprovalAction models
The package sends Filament database notifications for:
The plugin registers two widgets:
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.
Stats overview with:
Disable widgets:
FilamentApprovalPlugin::make()
->widgets(false)
Disable the flow resource:
FilamentApprovalPlugin::make()
->flowResource(false)
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
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).
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.
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 |
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 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.
// 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' => '',
];
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 labelsTo add a new language, create lang/vendor/filament-approval/{locale}/approval.php with the same structure.
If you have a custom Filament theme, add the package views to your @source directive:
@source '../../../../vendor/wezlo/filament-approval/resources/views/**/*';
php artisan test --filter=ApprovalEngine
MIT
How can I help you explore Laravel packages today?