Filament v3 Custom Actions: Bulk, Table, and Header Actions Done Right
Filament ships with a generous set of built-in actions, but production apps quickly outgrow them. You need bulk operations that dispatch jobs, header actions that resolve services from the container, and row-level actions that enforce policy before they run. This article shows concrete patterns for each.
1. Extracting Actions Into Dedicated Classes
Inlining action logic inside getTableActions() works for demos. For anything real, extract to a class:
// app/Filament/Actions/ArchiveOrderAction.php
namespace App\Filament\Actions;
use App\Jobs\ArchiveOrderJob;
use App\Models\Order;
use Filament\Tables\Actions\Action;
class ArchiveOrderAction
{
public static function make(): Action
{
return Action::make('archive')
->label('Archive')
->icon('heroicon-o-archive-box')
->requiresConfirmation()
->authorize(fn (Order $record) => auth()->user()->can('archive', $record))
->action(function (Order $record): void {
dispatch(new ArchiveOrderJob($record->id));
})
->successNotificationTitle('Order queued for archiving');
}
}
Register it in your resource:
public function getTableActions(): array
{
return [
ArchiveOrderAction::make(),
EditAction::make(),
];
}
The authorize() callback receives the resolved model, so policy checks stay close to the action without touching the resource.
2. Bulk Actions That Dispatch Batched Jobs
Bulk actions receive a Collection of selected records. Pair them with Bus::batch() for reliable fan-out:
use Filament\Tables\Actions\BulkAction;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Bus;
BulkAction::make('reprocess')
->label('Reprocess Selected')
->icon('heroicon-o-arrow-path')
->requiresConfirmation()
->deselectRecordsAfterCompletion()
->action(function (Collection $records): void {
$jobs = $records->map(
fn ($record) => new ReprocessOrderJob($record->id)
)->all();
Bus::batch($jobs)
->name('reprocess-orders')
->allowFailures()
->dispatch();
});
allowFailures() means one bad record does not abort the entire batch — critical when operating on hundreds of rows.
3. Header Actions With Service Injection
Header actions live outside the table loop, making them ideal for operations that need infrastructure services:
use Filament\Actions\Action;
use App\Services\ReportExporter;
protected function getHeaderActions(): array
{
return [
Action::make('export')
->label('Export CSV')
->action(function (ReportExporter $exporter): \Symfony\Component\HttpFoundation\StreamedResponse {
return $exporter->streamCsv(
$this->getTableQuery()->get()
);
}),
];
}
Filament resolves the ReportExporter from the service container automatically via the action() closure's type-hint — no manual app() call needed.
4. Guarding Actions With Visibility vs. Authorization
Two separate concerns are often conflated:
visible()— controls whether the action renders at all (UI concern).authorize()— throws a 403 if the action is invoked without permission (security concern).
Always set both:
Action::make('refund')
->visible(fn () => auth()->user()->hasRole('billing'))
->authorize(fn (Order $record) => auth()->user()->can('refund', $record))
->action(fn (Order $record) => $record->issueRefund());
Relying only on visible() is a security hole — a crafty user can POST the action directly.
5. Passing Extra Data Through mountUsing and form
When an action needs user input before executing, combine form() with mountUsing() to pre-populate fields:
Action::make('reschedule')
->form([
DateTimePicker::make('scheduled_at')->required(),
])
->mountUsing(fn (ComponentContainer $form, Order $record) =>
$form->fill(['scheduled_at' => $record->scheduled_at])
)
->action(fn (array $data, Order $record) =>
$record->update(['scheduled_at' => $data['scheduled_at']])
);
Takeaways
- Extract actions to dedicated classes early; inline closures do not scale.
- Use
Bus::batch()inside bulk actions for reliable, observable fan-out. - Header action closures support full container injection via type-hints.
- Always pair
visible()withauthorize()— they are not interchangeable. mountUsing()lets you pre-fill action forms from the current record state.