The Problem With Default Bulk Actions
Filament ships with DeleteBulkAction and a handful of helpers, but real applications need bulk actions that:
- Ask for extra input before running (e.g. a reason, a target status)
- Dispatch work to a queue instead of blocking the request
- Show the user meaningful feedback when the batch finishes
This article walks through all three concerns with production-ready code.
1. Custom Confirmation Modal With Extra Fields
The requiresConfirmation() helper gives you a yes/no dialog. For richer input, swap to form() on the action:
BulkAction::make('archive')
->label('Archive Selected')
->icon('heroicon-o-archive-box')
->form([
Textarea::make('reason')
->label('Archive reason')
->required()
->maxLength(500),
])
->action(function (Collection $records, array $data): void {
ArchiveRecordsJob::dispatch(
$records->modelKeys(),
$data['reason'],
auth()->id(),
);
})
->deselectRecordsAfterCompletion()
->successNotificationTitle('Archive queued')
->color('warning');
The form() closure receives validated $data alongside $records. Filament renders the fields inside the confirmation modal automatically — no custom Livewire component needed.
2. Dispatching a Laravel Job Batch
Passing raw model keys (not Eloquent models) to the job keeps the serialized payload small and avoids stale model state:
// app/Jobs/ArchiveRecordsJob.php
final class ArchiveRecordsJob implements ShouldQueue
{
use Queueable, Dispatchable, InteractsWithQueue, SerializesModels;
public function __construct(
private readonly array $ids,
private readonly string $reason,
private readonly int $actorId,
) {}
public function handle(): void
{
Post::whereIn('id', $this->ids)
->lazyById(200)
->each(function (Post $post): void {
$post->archive($this->reason, $this->actorId);
});
}
}
For very large selections, split into a job batch so each chunk runs independently and failures are isolated:
->action(function (Collection $records, array $data): void {
$chunks = array_chunk($records->modelKeys(), 100);
$batch = Bus::batch(
collect($chunks)->map(
fn (array $ids) => new ArchiveRecordsJob($ids, $data['reason'], auth()->id())
)->all()
)
->name('archive-posts-' . now()->timestamp)
->allowFailures()
->dispatch();
// Persist batch ID so the UI can poll it
session()->put('archive_batch_id', $batch->id);
})
3. Real-Time Progress Feedback
Store the batch ID in the session (or a DB record keyed to the user) and expose a Livewire polling component in your Filament page footer via a render hook:
// AppServiceProvider::boot()
Filament::registerRenderHook(
PanelsRenderHook::BODY_END,
fn (): View => view('filament.batch-progress'),
);
{{-- resources/views/filament/batch-progress.blade.php --}}
@if(session('archive_batch_id'))
<div wire:poll.2s="checkBatch">
@livewire('batch-progress-indicator', [
'batchId' => session('archive_batch_id')
])
</div>
@endif
The Livewire component calls Bus::findBatch($this->batchId) and exposes $batch->progress() (0–100) and $batch->finished(). When finished, dispatch a browser event to trigger a Filament notification and clear the session key.
4. Safety Details Worth Getting Right
Authorization — always gate the action:
->authorize(fn (): bool => auth()->user()->can('archive', Post::class))
Chunk size — lazyById inside the job prevents loading thousands of models into memory at once. Tune the chunk size to your row width.
allowFailures() — without this, a single failing job cancels the entire batch. For archiving, partial success is usually acceptable; log failures via ->catch() on the batch.
Idempotency — if a job retries, re-archiving an already-archived post should be a no-op. Guard with a status check at the top of handle().
Takeaways
- Use
form()onBulkActionfor confirmation modals that collect extra input before dispatch. - Pass model keys (not models) to queued jobs to keep payloads lean.
- Split large selections into a
Bus::batch()withallowFailures()for resilient processing. - Persist the batch ID and poll
Bus::findBatch()in a Livewire component for live progress. - Always authorize bulk actions explicitly; Filament does not infer policy checks automatically.