Actions
Actions are what can be done with a record (or with the whole Resource). They are declared inside actions():
public function actions(): array
{
return [
ViewAction::make(),
EditAction::make(),
DeleteAction::make()->bulk(),
RestoreAction::make(),
ForceDeleteAction::make(),
Action::make('publish')
->icon('check')
->requiresConfirmation()
->bulk()
->handle(function (array $data, Post $post): array {
$post->update(['is_published' => true]);
return $post->only(['id', 'title', 'is_published']);
}),
Action::make('purge_drafts')
->label('Purge drafts')
->icon('trash')
->standalone()
->requiresConfirmation()
->handle(function (array $data): array {
return ['deleted' => Post::where('is_published', false)->delete()];
}),
];
}The Action API
| Method | Description |
|---|---|
make(string $name) | Internal name of the action (it appears in the endpoint URL). |
label(string $label) | Button label. By default, Str::headline($name). |
icon(string $icon) | Icon name (see "Icons" below). |
standalone(bool = true) | Action on the whole Resource, not on a specific record (for example, "Purge drafts"). |
bulk(bool = true) | Can be applied to a selection of several records at once; it reuses the same per-record endpoint, there is no separate bulk API. |
requiresConfirmation(bool = true) | The frontend asks for confirmation before running it. |
withTrashed(bool = true) | The action operates on soft-deleted records (needed for restore/forceDelete). |
authorize(Closure $callback) | Decides whether the action is visible and runnable. It receives the same arguments as handle(). See "Authorization". |
schema(array<Field> $fields) | Fields of a form of the action's own. See "Actions with a form". |
url(Closure $callback, bool $openInNewTab = false) | Turns the action into a link. See "Actions that are a link". |
handle(Closure $callback) | The action's logic. It receives (array $data, Model $record) for actions on a record, or just (array $data) for standalone(). |
Authorization
- With
authorize(), the action uses that closure and nothing else. - Without
authorize(), the action requirescanEdit()on the record, orcanCreate()if it isstandalone(). An action that must be open to users who cannot edit has to declareauthorize()explicitly. - The built-in actions follow their own ability:
ViewActionwithcanView(),EditActionandRestoreActionwithcanEdit(),DeleteActionandForceDeleteActionwithcanDelete(). If you declareauthorize()on one of them, both conditions must be met. - A Resource that the user cannot list (
canViewAny()false) closes all its actions.
Actions with a form
schema() asks the user for data before running the action. A standalone() with schema() opens the form without any record:
Action::make('invite')
->label('Invite people')
->icon('paper-airplane')
->standalone()
->schema([
Tags::make('emails')
->itemRules('email')
->rules('required', 'array'),
Select::make('role')
->options(['editor' => 'Editor', 'viewer' => 'Viewer'])
->rules('required'),
])
->handle(function (array $data): array {
Invitations::send($data['emails'], $data['role']);
return ['message' => count($data['emails']).' invitations sent'];
})handle() receives the data already validated against the fields' rules. Validation errors come out with the field's label, not with its internal name. The Relation fields of an action only serve their options to users who can run it, and their query() is also applied when the form is submitted.
Action messages
If the handler returns an array with a message key of type text, the panel shows it to the user once the action is done. The remaining keys are returned to the frontend as they are. A handler that returns nothing answers { "success": true }.
Actions that are a link
With url(), an action does not run any handler: it navigates. The closure receives the record and returns the URL, or null to not show the action on that row.
Action::make('open_public')
->label('Open public page')
->icon('eye')
->url(fn (Post $post): ?string => $post->is_published ? route('posts.show', $post) : null, openInNewTab: true)Built-in actions
ViewAction, EditAction, DeleteAction, RestoreAction and ForceDeleteAction are Actions with a predefined icon (and, for the last two, withTrashed()/requiresConfirmation()):
ViewAction::make(); // icon 'eye'
EditAction::make(); // icon 'pencil'
DeleteAction::make(); // icon 'trash'
RestoreAction::make(); // icon 'arrow-uturn-left', withTrashed()
ForceDeleteAction::make(); // icon 'trash', withTrashed(), requiresConfirmation()DeleteAction, RestoreAction and ForceDeleteAction get a default handler if you declare none: delete/restore/force-delete the record through Eloquent (respecting SoftDeletes when the model uses it). You only need to call handle() explicitly when you want to replace that behaviour.
Blocking a deletion from the model: if a listener of your model's deleting event throws a RuntimeException (for example, "it still has dependents"), the delete endpoint catches it and answers 422 with the exception's message, instead of a bodyless 500 that the frontend cannot show. It is the same pattern Filament uses with DeleteAction::configureUsing().
protected static function booted(): void
{
static::deleting(function (AcademicYear $year): void {
if ($year->courses()->exists()) {
throw new RuntimeException('This course still has subjects.');
}
});
}AttachAction and DetachAction are specific to Relation managers; they are explained there in detail.
Icons
icon() takes a name from a curated subset of Heroicons that Arrel already ships. It is not all of Heroicons: only the icons Arrel itself uses in its own actions, widgets and navigation, so as not to pay bundle size for icons that no panel uses.
eye, trash, pencil, pencil-square, check, plus, plus-circle, x-mark, inbox, paper-airplane, key, envelope-open, arrow-uturn-left, document-text, home, arrow-right-on-rectangle, academic-cap, book-open, shield-check, squares-2x2, user-group, users, clipboard-document-check, bars-3, cog-6-tooth, chart-bar, calendar-days, calendar, chat-bubble-left-right, document-duplicate, information-circle, clock, user, exclamation-triangle, arrow-down-tray and bell.
A name outside this list is not rendered. The icons live in resources/js/support/icons.ts in the package.
Standard actions vs. custom actions
ViewAction/EditAction/DeleteAction do nothing magical: they are Actions with convenient defaults. Any custom action is defined in exactly the same way, with its own schema() and handle():
Action::make('rename')
->icon('pencil-square')
->schema([
Text::make('title')->rules('required', 'string', 'max:255'),
])
->handle(function (array $data, Post $post): array {
$post->update(['title' => $data['title']]);
return $post->only(['id', 'title']);
})