Skip to content

Filament v4 Table Bulk Actions: Custom Confirmation Modals and Scoped Authorization

Go beyond the default delete bulk action. Learn how to build custom Filament v4 bulk actions with typed confirmation modals, per-record authorization checks, and clean job dispatch patterns.

Published · Updated · 4 min read

Filament v4 Table Bulk Actions: Custom Confirmation Modals and Scoped Authorization

Why Bulk Actions Deserve More Attention

Most Filament tutorials show DeleteBulkAction and move on. In real applications, bulk actions are where business logic concentrates — mass approvals, batch exports, multi-record state transitions — and they need proper authorization, user feedback, and reliable async dispatch.

This article focuses on Filament v4's bulk action API, which ships with the unified Schema system and a cleaner modal builder.


Defining a Custom Bulk Action

Register bulk actions inside table() on your resource's ListRecords page or directly in the Table definition:

use Filament\Tables\Actions\BulkAction;
use Illuminate\Database\Eloquent\Collection;

BulkAction::make('approve')
    ->label('Approve Selected')
    ->icon('heroicon-o-check-circle')
    ->color('success')
    ->requiresConfirmation()
    ->modalHeading('Approve Applications')
    ->modalDescription('Only applications you are authorised to approve will be processed.')
    ->modalSubmitActionLabel('Approve Now')
    ->action(function (Collection $records): void {
        $records
            ->filter(fn ($r) => auth()->user()->can('approve', $r))
            ->each(fn ($r) => ApproveApplicationJob::dispatch($r));
    })
    ->deselectRecordsAfterCompletion()

The requiresConfirmation() call renders a modal automatically. The modalDescription string is the right place to set user expectations about partial authorization.


Per-Record Authorization Inside the Action

Filament's authorize() callback controls whether the bulk action button is visible, but it does not filter individual records. Always filter inside action() itself:

->authorize(fn (): bool => auth()->user()->can('approve-applications'))
->action(function (Collection $records): void {
    $authorized = $records->filter(
        fn ($record) => auth()->user()->can('approve', $record)
    );

    if ($authorized->isEmpty()) {
        Notification::make()
            ->title('Nothing to approve')
            ->warning()
            ->send();
        return;
    }

    $authorized->each(fn ($r) => ApproveApplicationJob::dispatch($r));

    Notification::make()
        ->title("{$authorized->count()} application(s) queued for approval")
        ->success()
        ->send();
})

This pattern prevents privilege escalation when a user selects records they own alongside records they do not.


Adding a Custom Confirmation Form

Sometimes you need extra input before the action runs — a reason field, a scheduled date, a confirmation string. Filament v4 lets you attach a Schema-based form to any bulk action modal:

use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\DatePicker;

BulkAction::make('schedule_review')
    ->label('Schedule Review')
    ->form([
        DatePicker::make('review_date')
            ->label('Review Date')
            ->required()
            ->minDate(today()->addDay()),
        Textarea::make('notes')
            ->label('Internal Notes')
            ->maxLength(500),
    ])
    ->action(function (Collection $records, array $data): void {
        $records->each(
            fn ($r) => $r->update([
                'review_date' => $data['review_date'],
                'review_notes' => $data['notes'],
                'status' => 'pending_review',
            ])
        );
    })
    ->deselectRecordsAfterCompletion()

The $data array is automatically validated against the form rules before action() fires.


Chunking Large Selections Safely

When a user selects thousands of records, loading them all into a Collection will exhaust memory. Dispatch a single job that re-queries by IDs instead:

->action(function (Collection $records): void {
    $ids = $records->modelKeys();

    ProcessBulkApprovalJob::dispatch($ids, auth()->id());

    Notification::make()
        ->title('Bulk approval queued')
        ->body(count($ids) . ' records will be processed in the background.')
        ->success()
        ->send();
})

Inside ProcessBulkApprovalJob, chunk the IDs:

public function handle(): void
{
    $approver = User::findOrFail($this->approverId);

    collect($this->ids)->chunk(100)->each(function ($chunk) use ($approver) {
        Application::whereIn('id', $chunk)
            ->get()
            ->filter(fn ($r) => $approver->can('approve', $r))
            ->each(fn ($r) => $r->markApproved($approver));
    });
}

Key Takeaways

  • Always filter records by policy inside action() — the authorize() callback only gates visibility.
  • Use ->form([...]) on a BulkAction to collect structured input before execution.
  • For large selections, pass only IDs to a queued job and re-query with chunking.
  • deselectRecordsAfterCompletion() prevents stale checkbox state after async operations.
  • Send contextual Notification feedback so users know when partial authorization reduced the scope.

Frequently asked questions

Does Filament's authorize() callback on a BulkAction filter individual records?

No. The authorize() callback only controls whether the action button is rendered for the current user. You must filter individual records inside the action() closure using your policy or gate checks.

How do I pass extra user input to a Filament bulk action?

Attach a ->form([...]) array of Filament form components to the BulkAction. The validated data is injected as a $data array into the action() closure automatically.

What is the safest way to handle bulk actions on thousands of records in Filament?

Extract only the record IDs from the Collection using modelKeys(), then dispatch a single queued job with those IDs. Inside the job, re-query and chunk the IDs to avoid memory exhaustion.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project