datomatic/nova-detached-actions
Detach Laravel Nova actions from resources and run them from a dedicated page or menu item, with no row selection required. Great for global tasks like imports, exports, or cleanup jobs, keeping resource toolbars and index views uncluttered.
This is a Nova tool, so everything starts in NovaServiceProvider. Install, register, then read the source — the README is thin and the package is small enough (~50 lines of glue) that vendor/datomatic/nova-detached-actions/src/ is the real documentation.
composer require datomatic/nova-detached-actions
Pin the version explicitly. v3.x targets Nova 5, earlier lines target Nova 4 — verify the laravel/nova constraint in the package composer.json against your pinned Nova before anything else. Nova is closed-source and version-locked, so this is the actual feasibility gate, not code complexity.
Register the tool in NovaServiceProvider::tools() (the package's service provider is auto-discovered; tools that ship compiled Vue assets also need php artisan nova:publish):
public function tools(): array
{
return [
new \Datomatic\NovaDetachedActions\NovaDetachedActions(),
];
}
Where to look first, in order:
src/ — find the base action class (the one you extend) and the service provider. That tells you the registration contract in one read.resources/js/ — this is where toolbar placement actually happens, and it is what will break on the next Nova major.composer.json — confirm laravel/nova and PHP constraints.ActionEvent behaviour — grep for how run events are recorded, because audit history is the least obvious thing to verify.First use case: a low-risk, idempotent, whole-dataset operation where there is genuinely nothing to select — clear the application cache, trigger a sync, regenerate a report. Define it as a detached action, attach it to the toolbar, and prove four behaviours on a staging Nova instance before writing a second one: it renders, the confirmation modal fires, authorizedToRun() actually gates it, and a queued run lands in the queue rather than blocking the request.
Extend the package's base action, don't retrofit standard ones. The cleanest pattern is a dedicated class per global operation, extending whatever base class the package exposes (typically a thin subclass of Laravel\Nova\Actions\Action). Keep these in app/Nova/Actions/Detached/ so the no-selection contract is visible from the namespace alone. A reviewer seeing that path should immediately ask the two questions that matter: does this action assume a selection? and who is allowed to run it?
Porting an existing action is a handle() rewrite. A standard Nova action receives the selected models as its first argument. A detached action has no selection, so handle() must resolve its own scope explicitly — an Eloquent query, a tenant ID from the authenticated user, a config value. Audit every ported action for $models assumptions before it ships; a silent no-op on an empty collection is the failure mode, not an exception.
Authorization is a first-class concern, not a default. Wrap every detached action with an explicit authorizedToRun(Request $request) that checks a policy or gate. Do not rely on the resource's implicit policy lookup, because there is no resource context to inherit from. A toolbar button that bypasses authorization is a privilege-escalation surface, and it is the one thing you must test explicitly rather than assume.
Queue anything that touches the whole dataset. Detached actions invite "run once over everything" operations, and a synchronous full-table export will hit PHP's max_execution_time in the request that triggered it. Set $this->shouldQueue() on anything that writes or scans more than a trivial number of rows, and verify the queue worker on the connection the action declares — a long-running action silently piling up on a stalled worker is worse than an obvious timeout.
Reuse the pipeline, not just the class. Confirmation dialogs, custom fields, response handling, and shouldQueue all come from Nova's action pipeline and should behave identically for a detached action. Lean on them rather than building parallel UI. ActionEvent recording is the one piece to verify by hand rather than infer — check a row appears in the audit table after a detached run, because that is what you will reach for during incident forensics.
Place them where operators will look. Toolbar placement on the index of the most obviously related resource is usually right even when the action is global — a "rebuild search index" button on the Products index is more discoverable than one buried in a settings page. Keep the count small; three buttons is a toolbar, ten is a wall.
No selection means no implicit scope, and that is a safety issue in multi-tenant installs. A detached action that resolves "all records" by default will cross tenant boundaries if you are not explicit. Resolve tenant from the authenticated user, not from $request->all().
Non-idempotent actions will get double-clicked. A toolbar button invites repeated clicks in a way a modal flow does not. Anything that mutates should either be safely re-runnable or lock via cache, a job uniqueness constraint, or a state flag.
Silent non-rendering after a Nova upgrade is the expected failure mode. The package's toolbar placement touches Nova's frontend layer, which makes Nova majors the primary breakage vector. Add a smoke test or a scheduled staging check that asserts the detached action appears in the toolbar — you will not get an error, just an empty space.
Publish assets and clear caches before debugging. php artisan nova:publish, php artisan view:clear, and a hard browser refresh resolve a surprising share of "the button isn't there" reports. If it is still missing, open the Vue devtools and confirm the package component is mounted at all before reading the package source.
Verify native Nova standalone actions don't already solve this on your pinned version. Nova has been steadily improving standalone and toolbar action support. If the gap has closed, you are adding a single-maintainer, eleven-star dependency for nothing.
Treat this as source you own, not a product you bought. One maintainer, no dependents, no SLA. Fork or vendor it behind your own namespace, record the fork intent in your dependency register, name an owner, and set a trigger — six months of upstream silence, or a Nova upgrade that breaks it — at which you reimplement rather than scramble. The surface area is small enough that reimplementation is a legitimate, cheap escape hatch.
Read the source before you trust the abstraction. With no ecosystem validation, nothing downstream will surface a break before you do. The package is small; spending the twenty minutes reading it end-to-end is cheaper than debugging it blind in production.
How can I help you explore Laravel packages today?