Actions
Adminix action metadata is the shared contract for existing action-like modules. It does not replace current execution flows; links, modal togglers, and bulk actions still run through their existing routes, Blade views, and handlers.
AdminixActionInterface exposes:
ActionMetadataDto contains:
name- stable action key;title- visible label;icon- Bootstrap icon class;tooltip- optional hover text;color- optionalColorsEnumvalue or CSS color string;destructive- boolean marker for dangerous actions;confirm- optional confirmation text;criteria- optional visibility criteria.
The contract is currently implemented by:
LinkModule;AdminixLinkModule;ModalTogglerModule;BulkAction;CallbackActionModule.
Execution boundaries
Action metadata is descriptive. Do not use it as authorization, persistence, or routing input by itself.
State-changing actions must still be resolved from server-side Adminix configuration:
row actions are configured through
ListModule::addAction()/addActions();bulk actions are configured through
ListModule::addBulkAction()/addBulkActions();built-in clone actions are enabled through
ListModule::cloneAction(),bulkCloneAction(), orcloneActions();callback actions are configured through
Module::callbackAction()/CallbackActionModule::make();modal togglers open configured modal modules;
selected IDs, page params, relation context, and list scope remain server-verified.
Future action features can build on ActionMetadataDto without making browser-submitted action metadata authoritative.
Security checklist
Action handlers run inside privileged admin routes. Keep every write tied to server-side Adminix configuration:
use
$request->query()for bulk writes instead of rebuilding a query from request input;read only declared action fields through
$request->field()/$request->fields();treat selected IDs as already filtered by Adminix, and never re-add raw browser IDs to a fresh datasource query;
do not read datasource names, primary keys, writable columns, tenant IDs, parent IDs, webhook URLs, or
param:*values from browser metadata;keep authorization, tenant/user ownership, and destructive-action checks in server-side handlers or policies;
build redirect URLs, modal bodies, downloads, and new-tab targets from trusted server-side state.
Generating handlers
Use the action generator for reusable bulk action handlers:
The generated class lives in app/Adminix/Actions, implements BulkActionHandlerInterface, and receives a BulkActionRequest. The optional test stub lives in tests/Feature/Adminix/Actions.
make:adminix_action currently supports --type=bulk. The generated handler is intentionally not registered automatically; attach it to a server-defined action after reviewing the code:
Callback actions
Use CallbackActionModule for page-level buttons or forms that call trusted server code without selecting list rows. Good examples are "sync with CRM", "send webhook", "recalculate totals", "start maintenance check", or "publish current tenant settings".
Result: /adminix/tenants/15 renders a normal Adminix form. On submit, Adminix verifies the signed page/module context, re-resolves the configured module from PHP, normalizes only declared fields, runs authorizeUsing(), calls the handler, records callback.executed, and redirects back with the configured response. Extra browser-submitted field keys are ignored. Invalid or tampered callback context returns a controlled error instead of calling the handler.
Rendered example:

For simple outbound HTTP callbacks, configure the webhook URL on the server:
Adminix posts the payload through Laravel's HTTP client. The browser never chooses the webhook URL, headers, payload shape, datasource, tenant id, or handler class. Use application services for retries, idempotency keys, secrets, and provider-specific signing.
Action fields
BulkAction and CallbackActionModule can declare input fields that are shown before the action is submitted. Adminix renders bulk fields only when the matching bulk action is selected, and renders callback fields inside the callback module form. During execution, Adminix reads only fields declared on the selected server-side action; forged extra keys are ignored.
Bulk handlers read normalized values from BulkActionRequest:
Callback handlers use the same field accessors on CallbackActionRequest:
Supported field types:
STRING;TEXTAREA;INTEGER;FLOAT;BOOLEAN;DATE;DATETIME;SELECT.
Server-side normalization:
unknown field keys are ignored;
required fields are checked before the handler runs;
integer and float fields must contain numeric scalar values;
boolean fields are normalized to
true/false;select fields must match one of the configured
SelectRecordvalues when options are configured.
Action fields are handler input only. They do not change list scope, selected IDs, authorization, datasource, or writable columns.
Queued bulk actions
Use Laravel queues for slow bulk handlers:
Queued actions use the same form and endpoint as normal bulk actions. Before dispatching the job, Adminix verifies the signed page params, optional relation context, selected IDs, current list scope, declared action fields, batch limit, and action authorization. For authenticated panels it also records a server-derived actor snapshot containing only the selected guard name and the actor's scalar authentication identifier. The user model and request are never serialized into the job. For an asynchronous queue connection, the HTTP request returns the queued() message as the normal success toast and Adminix records bulk.queued after successful dispatch. When the selected connection, or queue.default when none is selected, uses Laravel's sync driver, Adminix runs the same authorized job path inline and returns the worker's actual BulkActionResult. That path records bulk.executed only; it does not report a false queued acknowledgement or emit bulk.queued. The real job is dispatched through the selected Laravel queue connection, so payload hooks, serialization, JobProcessing/JobProcessed events, attempts, and connection-level after_commit behavior still apply. If effective after_commit is enabled while a database transaction is open, the sync job cannot provide a result before that transaction finishes. Adminix therefore returns the honest queued() acknowledgement, records bulk.queued, and runs the handler only after commit; rollback discards the deferred job. With no pending transaction, the same connection still runs inline and returns the actual result.
When the job runs, Adminix re-resolves the page, module, relation context, and action from server-side configuration. On authenticated panels it first reloads the actor through the captured guard's current user provider, requires AdminixUserInterface, and rechecks the actor's current getAdminCriteria() values. It then rechecks current module visibility and BulkAction::authorizeUsing(), reapplies the saved query parameters for filters, search, lenses, and sorting, and filters selected IDs against the current server-side list scope before calling the handler. Missing, malformed, deleted, or no-longer-admin actor context fails with a controlled error and the handler is not called. Panels with no_auth_access keep actorless queue execution for local/demo use. After the handler finishes, Adminix records the normal bulk.executed audit event with the handler result. Controlled stale context, configuration, validation, scope, and authorization rejections return a BulkActionResult and complete without a retry. On asynchronous connections, an unexpected exception from the application handler is not converted to a result in the queued worker: the original exception reaches Laravel so the configured attempts, backoff, and terminal failure handling apply. The sync driver has no later worker attempt, so the initiating HTTP request receives one controlled error result after the single inline invocation. Direct, non-queued HTTP execution keeps the same controlled error behavior.
Restart long-lived Laravel queue workers as part of the Adminix upgrade deployment so they load the authorized job classes and actor-aware parent jobs together. Jobs queued by an older Adminix version have no actor snapshot and fail closed on authenticated panels after the upgrade; redispatch those actions from a fresh authenticated request.
Queued action options:
queued('Message')enables queued execution and sets the immediate toast message.progressTrigger('archive-progress')starts a matchingProgressBarModuletrigger in the browser.onConnection('redis')optionally chooses the Laravel queue connection.onQueue('adminix')optionally chooses the queue name.
Queued handlers receive the same BulkActionRequest contract. They should return success, error, or validation results for logs and future notification/progress integrations. Browser response types such as redirects, downloads, and new-tab handoffs are useful for direct handlers and queued actions using the sync driver. Asynchronous workers run after the original HTTP request has ended.
Safe queued handlers should be idempotent. Laravel may redeliver a handler after it throws, including after partial application writes. Adminix assigns a UUID when it dispatches the job. BulkActionRequest::executionId() returns that same value after queue serialization and on every delivery of the job:
Back the receipt with a unique database constraint and return its stored outcome on redelivery. The execution ID is a stable application key, not an atomic deduplication mechanism: Adminix cannot make an external API call or a consuming application's separate transaction exactly-once. Adminix dispatches best-effort QueuedExecutionStarted and QueuedExecutionFinished framework events around each worker attempt. Their QueuedExecutionEventDto exposes the execution ID, kind, panel/page/module/action names, attempt, lifecycle/result status, actor type/ID, and result issue count. It intentionally excludes selected IDs, action-field values, query/signed contexts, result messages, and credentials. A listener exception is ignored and cannot change the handler result or Laravel retry behavior. An unexpected exception has no normal finished event; Laravel retry handling continues, and the terminal failed() callback emits a sanitized failed finish event. Update progress/result state from the queue worker rather than from the progress handler. Re-check tenant/user authorization and current record state inside application services before destructive writes. For generated files, write to an application-owned disk or object store, return only signed/download URLs from trusted server state, and define retention/cleanup outside the browser request.
Action responses
BulkActionResult and CallbackActionResult support the normal message outcomes and typed response outcomes:
Response behavior:
success,error, andvalidationredirect back with the normal Adminix toast and validation errors;redirectredirects to the server-provided URL with the normal session toast;openInNewTabreturns a small safe handoff page that attempts to open the server-provided URL and includes fallback links;modalredirects back, renders a session-backed Bootstrap modal, and shows the normal toast;downloadreturns the provided Symfony/Laravel response directly.
The response target is resolved by the server-side handler. Do not derive redirect URLs, modal content, download responses, or new-tab targets from browser-owned datasource, primary key, writable fields, or tenant context.