Key takeaways
- ✓ Since v4 there is one
Filament\Actions\Actionclass. The same action works in tables, page headers, forms and infolists. - ✓ Scale confirmation to risk:
requiresConfirmation()for routine changes, a typed confirmation for destructive ones, and a modal form when you need a reason or details. - ✓ Bulk actions receive a
Collectionof records. For large selections, usechunkSelectedRecords()and authorise records individually withauthorizeIndividualRecords(). - ✓
->visible()controls UX,->authorize()controls security. Use both on anything that matters.
Actions are the verbs of a Filament panel: approve, refund, archive, send, export. In Filament v3 they were split across several classes (table actions, page actions, form actions). Since v4 there’s a single Filament\Actions\Action that works everywhere, and the modal API has grown into a small UI toolkit of its own. This guide collects the patterns worth standardising on in a v5 panel, from the simplest to the most careful.
Also Read: Laravel and PHP
Where actions live
| Location | Method | Typical actions |
|---|---|---|
| Table row | ->recordActions([...]) | Edit, Approve, Refund |
| Table header | ->headerActions([...]) | Create, Import, Export |
| Table toolbar (bulk) | ->toolbarActions([...]) | Bulk delete, bulk assign |
| Resource page header | getHeaderActions() | Delete, Duplicate, Send |
| Inside a schema | Actions::make([...]) or a field’s ->suffixAction() | Generate slug, Fetch address |
Because it’s the same class everywhere, an approve action you build for a table row can be reused on the view page header.
Pattern 1: Simple confirmation
For reversible, low-risk actions:
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Support\Icons\Heroicon;
Action::make('publish')
->icon(Heroicon::OutlinedEye)
->color('success')
->requiresConfirmation()
->modalHeading('Publish this post?')
->modalDescription('It will be visible on the website immediately.')
->modalSubmitActionLabel('Yes, publish')
->visible(fn (Post $record) => $record->isDraft())
->authorize('publish')
->action(function (Post $record): void {
$record->publish();
Notification::make()->title('Post published')->success()->send();
}),authorize('publish') calls PostPolicy::publish(). If the policy says no, the action is hidden and can’t be triggered by a crafted Livewire request. visible() only handles the UI state. Our guide to roles and permissions in Filament covers the policy side.
Pattern 2: Typed confirmation for destructive actions
“Are you sure?” dialogs get clicked through out of habit. For actions you can’t undo, such as deleting a workspace or revoking every API key, make the user type something specific:
use Filament\Forms\Components\TextInput;
Action::make('deleteWorkspace')
->color('danger')
->icon(Heroicon::OutlinedTrash)
->modalIcon(Heroicon::OutlinedExclamationTriangle)
->modalHeading('Delete workspace permanently')
->modalDescription('This deletes all projects, files and members. It cannot be undone.')
->schema([
TextInput::make('confirmation')
->label(fn (?Workspace $record) => "Type \"{$record?->slug}\" to confirm")
->required()
->in(fn (?Workspace $record): array => [$record?->slug])
->validationMessages(['in' => 'That does not match the workspace name.']),
])
->modalSubmitActionLabel('Delete forever')
->authorize('forceDelete')
->action(fn (Workspace $record) => $record->forceDelete()),Validation runs on the server, so the check can’t be bypassed from the browser.
Also Read: Laravel and PHP
Pattern 3: Modal forms that collect details
When the action needs input, such as a refund amount or a rejection reason, use schema():
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
Action::make('refund')
->icon(Heroicon::OutlinedReceiptRefund)
->fillForm(fn (Order $record): array => [
'amount' => $record->refundable_amount,
])
->schema([
TextInput::make('amount')
->numeric()
->prefix('£')
->required()
->maxValue(fn (?Order $record) => $record?->refundable_amount),
Select::make('reason')
->options(RefundReason::class)
->required(),
Textarea::make('note')->rows(3),
])
->modalWidth(\Filament\Support\Enums\Width::Large)
->action(function (array $data, Order $record): void {
$record->refund($data['amount'], $data['reason'], $data['note'] ?? null);
}),fillForm() pre-fills the fields. $data receives validated input. Since v4, enum-backed fields always give you an enum instance, so $data['reason'] is a RefundReason, not a string.
Pattern 4: Wizards for multi-step flows
For longer flows, such as onboarding a customer or configuring an integration, swap schema() for steps():
use Filament\Schemas\Components\Wizard\Step;
Action::make('onboard')
->steps([
Step::make('Company')
->schema([
TextInput::make('company_name')->required(),
TextInput::make('vat_number'),
]),
Step::make('Billing')
->schema([
Select::make('plan')->options(Plan::class)->required(),
]),
Step::make('Invite')
->schema([
TextInput::make('admin_email')->email()->required(),
]),
])
->action(fn (array $data) => app(OnboardCustomer::class)($data)),Each step validates before the user can move on. The final $data contains every step’s fields.
Pattern 5: Slide-overs for context-heavy work
Wide modals hide the table the user is working from. A slide-over keeps the list visible:
Action::make('viewTimeline')
->slideOver()
->modalContent(fn (Order $record) => view('filament.orders.timeline', ['order' => $record]))
->modalSubmitAction(false)
->modalCancelActionLabel('Close'),Add stickyModalHeader() and stickyModalFooter() for long content, and closeModalByClickingAway(false) for forms where losing input would hurt.
Also Read: Best Filament Themes and PHP Starter Kits for 2026 (v5-Ready)
Pattern 6: Nested and chained actions
Modals can contain actions that open their own modals. Filament stacks them automatically. A common example is a “Reject” action with an extra footer button that sends the customer a message first:
Action::make('reject')
->schema([Textarea::make('reason')->required()])
->extraModalFooterActions(fn (Action $action): array => [
$action->makeModalSubmitAction('rejectAndNotify', arguments: ['notify' => true])
->label('Reject & email customer'),
])
->action(function (array $data, array $arguments, JobApplication $record): void {
$record->reject($data['reason']);
if ($arguments['notify'] ?? false) {
$record->customer->notify(new ApplicationRejected($data['reason']));
}
}),makeModalSubmitAction() submits the same form with extra arguments, so you avoid building two nearly identical actions. For child actions that should close their parent modal when they finish, use cancelParentActions().
Pattern 7: Bulk actions that won’t fall over
Bulk actions receive the selected records as a Collection:
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Illuminate\Database\Eloquent\Collection;
->toolbarActions([
BulkActionGroup::make([
BulkAction::make('assign')
->icon(Heroicon::OutlinedUserPlus)
->schema([
Select::make('agent_id')->relationship('agent', 'name')->required(),
])
->action(function (Collection $records, array $data): void {
$records->each->update(['agent_id' => $data['agent_id']]);
})
->deselectRecordsAfterCompletion(),
DeleteBulkAction::make()
->authorizeIndividualRecords('delete'),
]),
])Three things to get right with bulk actions:
- Memory. “Select all” on 40,000 rows loads 40,000 models. Use
->chunkSelectedRecords(250)so records are processed in chunks, or dispatch a queued job from the action with the selected IDs. - Authorisation. By default, bulk actions check the
*Anypolicy method once, for speed. If some records may be off limits (another team’s, or locked), useauthorizeIndividualRecords()to run the per-record policy. - Bypassing model events.
Model::whereIn(...)->update()is fast but skips observers, so your audit log never sees the change.$records->each->update()fires events. Choose deliberately.
Pattern 8: Grouping and keyboard shortcuts
Row actions add up quickly. Group secondary ones behind an ellipsis:
use Filament\Actions\ActionGroup;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
->recordActions([
EditAction::make(),
ActionGroup::make([
ViewAction::make(),
Action::make('duplicate')->action(fn (Post $record) => $record->replicate()->save()),
Action::make('archive')->requiresConfirmation()->action(fn (Post $record) => $record->archive()),
])->tooltip('More'),
])For power users, bind shortcuts to page actions with ->keyBindings(['command+s', 'ctrl+s']).
Pattern 9: Actions inside forms
Actions aren’t only buttons in tables and headers. Attach them to fields as affix actions, for example a “generate slug” button next to the slug input:
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Str;
TextInput::make('slug')
->required()
->suffixAction(
Action::make('generateSlug')
->icon(Heroicon::OutlinedSparkles)
->tooltip('Generate from title')
->action(fn (Get $schemaGet, Set $schemaSet) => $schemaSet('slug', Str::slug($schemaGet('title')))),
),Inside schemas, actions can inject $schemaGet and $schemaSet to read and write other fields’ state. Use the parameter names exactly, because Filament injects utilities by name.
Also Read: Filament Blueprint Review (2026): Is PHP It Worth It for AI Agents?
Pattern 10: Stopping an action partway through
Sometimes you only find out inside the action that it can’t go ahead: the order was refunded by someone else a second ago, or an external API refused the request. Use halt() to keep the modal open with the user’s input intact, or cancel() to close it:
->action(function (array $data, Order $record, Action $action): void {
if (! $record->fresh()->isRefundable()) {
Notification::make()
->title('This order can no longer be refunded')
->body('It may have been refunded by someone else. Refresh to see the latest status.')
->danger()
->send();
$action->halt(); // modal stays open
}
$record->refund($data['amount']);
})Re-checking state with fresh() inside the action, not just in visible(), is what makes it safe against two admins clicking at the same time. For money, wrap the check and the change in a database transaction with a row lock (lockForUpdate()).
Notifications: tell users what happened
Every action that changes data should confirm it. Prebuilt actions have ->successNotificationTitle('Refund issued'). In custom actions, send a Notification yourself. For long-running work, queue a job and send a database notification when it finishes, so the user isn’t left watching a spinner.
Testing actions
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
it('refunds an order from the table', function () {
$order = Order::factory()->paid()->create(['total' => 100]);
livewire(ListOrders::class)
->callAction(TestAction::make('refund')->table($order), data: [
'amount' => 40,
'reason' => RefundReason::Damaged,
])
->assertHasNoFormErrors()
->assertNotified();
expect($order->fresh()->refunded_amount)->toEqual(40);
});
it('hides publish for published posts', function () {
$post = Post::factory()->published()->create();
livewire(ListPosts::class)
->assertActionHidden(TestAction::make('publish')->table($post));
});More patterns, including bulk actions with ->table()->bulk(), are in our Filament + Pest testing guide.
Also Read: Claude Code & Cursor on Filament: AI Agent Rules That Work
FAQ
How do I add a confirmation modal to a Filament action?
Call ->requiresConfirmation() on the action. Customise it with modalHeading(), modalDescription(), modalSubmitActionLabel() and modalIcon().
How do I add a form to a Filament action?
Pass form components to ->schema([...]). The validated input arrives as array $data in ->action(). Use ->fillForm() to set default values.
How do I process thousands of records in a bulk action?
Use ->chunkSelectedRecords() to process in chunks, or dispatch a queued job with the selected keys. Add authorizeIndividualRecords() if permissions vary per record.
What’s the difference between visible() and authorize()?
visible() / hidden() only affect whether the button is rendered. authorize() checks a policy ability and also blocks execution if the action is triggered directly.
Sources and further reading
- Filament actions overview
- Filament action modals
- Filament table actions and bulk actions
- Filament testing actions
- Laravel policies
Related on The Web Tier: CSV import and export in Filament · Soft deletes and trash views
