Skip to main content
Actions are the verbs of your application. They represent typed operations — approve an order, invite a user, start a test round — with built-in authorization, validation, and execution logic. An action that takes an Order parameter is an Order action. There is no separate registration step. Actions are domain primitives, not HTTP controllers. They return structured results that the platform interprets differently depending on context: a web request gets a redirect with a toast, an API call gets JSON, a bulk operation gets a summary. You write the business logic once.

Schema-driven

Parameters declared as Properties. Validation, forms, and API docs generated automatically.

Context-agnostic

Same action works from page buttons, table rows, API calls, and AI agents.

Self-describing

Label, icon, variant, preconditions, and modal preferences live on the action class.

Quick Start

1. Create the Action

For standard actions (edit, create, delete, restore, duplicate), use --object and --type to scaffold a full action in one command. --type accepts either a single value or a comma-separated list:
php artisan make:core-action --object=Order --type=edit
php artisan make:core-action --object=Order --type=create
php artisan make:core-action --object=Order --type=delete
php artisan make:core-action --object=Order --type=restore
php artisan make:core-action --object=Order --type=duplicate

# Generate multiple standard actions at once
php artisan make:core-action --object=Order --type=edit,delete
TypeCommand
Editmake:core-action --object=Order --type=edit
Createmake:core-action --object=Order --type=create
Deletemake:core-action --object=Order --type=delete
Restoremake:core-action --object=Order --type=restore
Duplicatemake:core-action --object=Order --type=duplicate
Add --bulk to enable table bulk actions. When used with a comma-separated --type list, it applies to every generated standard action. For custom actions (e.g. approve, cancel), pass the class name and optionally --object:
php artisan make:core-action Order/ApproveOrderAction --object=Order
app/Actions/Order/ApproveOrderAction.php
class ApproveOrderAction extends CoreAction
{
    protected static ?string $subjectClass = Order::class;

    public function authorize(): bool
    {
        return has_permission(Permission::MANAGE_ORDERS);
    }

    protected function defaults(): void
    {
        $this->label(__('Approve'))
            ->icon('Check')
            ->variant(CoreVariant::SUCCESS)
            ->confirm(__('Approve this order and notify the customer?'));
    }

    protected function handle(): ActionResult
    {
        $order = $this->subject();
        $order->approve();

        return $this->success(__('Order approved.'))->object($order);
    }
}

2. Use in a Controller

app/Http/Controllers/OrderController.php
public function show(Order $order)
{
    return AppPage::make('orders/show')
        ->title($order->name)
        ->actions([
            ApproveOrderAction::make($order),
        ]);
}
Pass the subject model as the first argument to make(). The platform serializes it to an ID for the frontend and resolves it back in handle() via $this->subject().

3. Render on the Frontend

resources/js/pages/orders/show.tsx
export default function OrderShow({ order }) {
  return (
    <AppPage>
      {/* Page content — action button appears in the header */}
    </AppPage>
  );
}
The approve button renders in the page header. Click it and the confirmation dialog appears. On confirm, the action executes, the user sees a success toast, and the page redirects to the order.

Schema

The schema() method declares an action’s parameters as Properties. This single definition drives validation rules, form rendering, API documentation, and TypeScript type generation.
protected function schema(): SchemaDefinition
{
    return SchemaDefinition::make([
        ObjectProperty::make('customer', __('Customer'))
            ->model(Customer::class)
            ->searchEndpoint(route('customers.search'))
            ->required(),

        DateProperty::make('ordered_at', __('Order date'))
            ->required(),

        StringProperty::make('notes', __('Notes'))
            ->nullable(),
    ]);
}

Subject and params in schema()

Your schema often depends on the bound subject or params — for example, enum options from $school->type, validation rules scoped to $school->id, or default values from bound params. That is fine at runtime when the action is constructed with a real subject. schema() must still run when there is no subject. Type generation (composer generate-ts), API introspection, and similar tooling instantiate the action and call schema() without binding a model. If you read the subject (or anything that assumes it exists) unconditionally, those steps will fail. Guard with null checks and fall back to a shape that is still valid for codegen and docs — same property names and types, with conservative defaults. $this->subject() already returns Model|null. On every property type you can use when() so the base definition stays valid without a subject and you only attach subject-specific options or rules when needed:
protected function schema(): SchemaDefinition
{
    $school = $this->subject();

    return SchemaDefinition::make([
        EnumProperty::make('type', __('Type'), ProgramType::class)
            ->required()
            ->when(
                $school !== null,
                fn (EnumProperty $property) => $property
                    ->options($school->type->programs())
                    ->addRule(
                        Rule::unique('programs', 'type')
                            ->where('school_id', $school->id)
                            ->whereNull('deleted_at'),
                    ),
            ),
    ]);
}
With no subject, EnumProperty::make(..., ProgramType::class) still supplies full enum options from the class; the callback only narrows options and adds the unique rule when a school is bound. The important part is: never assume the subject is present inside schema(). Use when(), null-safe access, or explicit if blocks so subject-scoped configuration runs only when the model is actually bound.

Reusing Object Schemas

When an action’s parameters overlap with a model’s schema, extract them directly. You can pick specific properties with only(), exclude with except(), and add action-specific properties with merge():
protected function schema(): SchemaDefinition
{
    return Order::schemaDefinition()
        ->only(['customer', 'ordered_at'])
        ->merge(SchemaDefinition::make([
            EnumProperty::make('priority', __('Priority'))
                ->options(Priority::class)
                ->required(),
        ]));
}

Subject-based actions

Actions that operate on a single model instance (update, delete, approve…) declare the model class via $subjectClass. The platform enforces that a subject is always provided at binding time, and makes it available in every action method via $this->subject():
class UpdateSchoolAction extends CoreAction
{
    protected static ?string $subjectClass = School::class;

    protected function schema(): SchemaDefinition
    {
        return School::schemaDefinition()
            ->only(['name', 'organization_no', 'city_id', 'website']);
    }

    public function authorize(): bool
    {
        return user()?->can('update', $this->subject()) ?? false;
    }

    protected function handle(): ActionResult
    {
        $this->subject()->update([
            'name'    => $this->string('name'),
            'city_id' => $this->integer('city_id'),
            'website' => $this->string('website')->value(),
        ]);

        return $this->success(__('School updated successfully'));
    }
}
Bind the subject at call-site using make():
// With just a subject
UpdateSchoolAction::make($school)

// With a subject and additional params
UpdateSchoolAction::make($school, ['extra' => 'value'])
If you call make() with a non-empty params array that doesn’t include a subject instance, the platform throws immediately — catching the omission at the call site rather than at runtime.

What $subjectClass does

  • $this->subject() — returns the bound model instance (lazy-resolves from a scalar ID if needed, then caches)
  • getSubjectClass() / getSubjectKey() — static introspection
  • make($model) / make($model, $params) — bind subject at construction time with type enforcement
  • setSubject($model) — used internally by tables and ObjectActionSet for per-row binding
  • resolveSubjectParamName() — derives from $subjectClass (or a setSubject() binding) only; the schema is never used to infer the subject

Edit / update forms

When $subjectClass is declared, the platform automatically extracts the subject model’s current attribute values and uses them as property values in the auto-form. No extra code is needed:
class UpdateSchoolAction extends CoreAction
{
    protected static ?string $subjectClass = School::class;

    protected function schema(): SchemaDefinition
    {
        // The current school attributes are automatically used as form values
        return School::schemaDefinition()
            ->only(['name', 'organization_no', 'city_id', 'website']);
    }
}
The form opens pre-filled. This works both when the model instance is passed from a controller and when the modal route receives a scalar ID from the query string.

Values, Defaults, and Bound Params

Action forms have three separate concepts:
  • Bound params: values passed via make(...), params(...), or frontend params/getParams. These are hidden automatically in schema-driven forms and drive subject resolution, preconditions, and direct execution.
  • Values: the current form state shown to the user. Subject-bound actions get these automatically from the model schema. You can also set them programmatically with ->value(...).
  • Defaults: fallback initial form state used only when no value exists. Set these with ->default(...).
The precedence is:
  1. Bound params
  2. Programmatic or subject-derived values
  3. Defaults
Use ->value(...) when you want to prefill the form from something other than the subject model, such as session state, flash data, or another bound param:
protected function schema(): SchemaDefinition
{
    return SchemaDefinition::make([
        StringProperty::make('email', __('Email'))
            ->required()
            ->value(session('draft_email')),

        StringProperty::make('notes', __('Notes'))
            ->value($this->get('template_note'))
            ->default(__('Add context for the recipient')),
    ]);
}
Use ->default(...) when you want a fallback only if the property does not already have a value:
protected function schema(): SchemaDefinition
{
    return SchemaDefinition::make([
        StringProperty::make('notes', __('Notes'))
            ->default(__('Initial note')),

        EnumProperty::make('priority', __('Priority'))
            ->enum(Priority::class)
            ->default(Priority::HIGH),
    ]);
}
The distinction matters:
  • Bound params signal completeness and are hidden automatically in schema-driven forms.
  • Values prefill the form but do not count as bound params.
  • Defaults are only used when no value is available.
Automatic subject-derived values only fill properties that do not already declare an explicit ->value(). If you set both ->value() and ->default(), the form uses the explicit value and keeps the default as a fallback.

Backwards compatibility

If an action defines rules() without schema(), it works as before. No auto-form or API schema is generated. When both exist, schema() takes precedence.

Accessing Parameters

Subject

When $subjectClass is declared, access the subject model from any action method via $this->subject(). It resolves lazily from a scalar ID if needed and caches the result:
protected function handle(): ActionResult
{
    $order = $this->subject(); // Order instance
    $order->approve();

    return $this->success(__('Order approved.'))->object($order);
}

public function authorize(): bool
{
    return auth()->user()->can('update', $this->subject());
}

protected function preconditions(): PreconditionResult
{
    $order = $this->subject();

    return PreconditionResult::check([
        __('Order is in draft') => $order->isDraft(),
        __('Order has line items') => $order->lines()->exists(),
    ]);
}

protected function schema(): SchemaDefinition
{
    return Order::schemaDefinition()->only(['notes', 'priority']);
}

Form field accessors

For form fields (the non-subject parameters), use $this->get() and its typed variants. These methods read the exact keys you provide:
protected function handle(): ActionResult
{
    $order = Order::create([
        'customer_id' => $this->integer('customer'),
        'ordered_at'  => $this->date('ordered_at'),
        'status'      => $this->enum('status', OrderStatus::class),
        'priority'    => $this->integer('priority'),
        'notes'       => $this->get('notes'),
        'internal_ref'     => $this->string('internal_ref'),
        'express_delivery' => $this->boolean('express_delivery'),
    ]);

    return $this->success(__('Order created.'))->object($order);
}

protected function schema(): SchemaDefinition
{
    return SchemaDefinition::make([
        ObjectProperty::make('order', __('Order'))->model(Order::class)->required(),
        StringProperty::make('email', __('Email'))
            ->rules(['unique:users,email,' . $this->get('order')?->id]),
    ]);
}
MethodReturnsDescription
subject()ModelThe declared $subjectClass instance
get(name)mixedRaw resolved value
date(name)CarbonParsed as Carbon instance
integer(name)intCast to integer
float(name)floatCast to float
boolean(name)boolCast to boolean
string(name)StringableCast to Stringable
enum(name, Enum::class)typed enumCast to enum
collect(name)CollectionCast to collection
validated()arrayFull resolved parameter bag
safe()ValidatedInputFor only(), except(), all()
validated() keeps the original schema property names exactly as defined. If your property is named max_characters, read it as max_characters via $this->validated(), $this->get(), $this->integer(), etc. The action layer does not add camelCase aliases for form fields.

Parameter Timing

Action methods run in two phases:
  • Initialization phase: defaults(), schema(), rules(), authorize(), and form() only have access to the bound subject and bound params.
  • Execution phase: handle() has the validated submission available through $this->get(), the typed accessors, and validated().
preconditions() can run in both phases:
  • while serializing actions for the UI, it only sees currently bound params
  • during execution, it runs again with the submitted, validated data
That means this is correct:
protected function schema(): SchemaDefinition
{
    return SchemaDefinition::make([
        StringProperty::make('notes', __('Notes'))
            ->value($this->get('template_note')),
    ]);
}

protected function handle(): ActionResult
{
    $notes = $this->get('notes'); // submitted form state

    // ...

    return $this->success();
}
$this->subject() and $this->get() are safe to use during initialization, but at that point they only reflect the subject plus any params you explicitly bound. Submitted form state does not exist until execution.

Return Values

Every handle() method returns an ActionResult. Two builder methods set the outcome, and chainable methods attach what happens next:
// Two builders
$this->success(?string $message = null)  // ActionResult with success: true
$this->error(string $message)            // ActionResult with success: false

// Chainable references
->object(ObjectContract $object)   // attach object — web auto-redirects to show page
->workflow(StoredWorkflow $wf)     // attach workflow reference for progress tracking
->redirectTo(string $url)          // custom redirect (overrides object URL if both set)
->message(string $message)         // set or override message

Common Patterns

// Create — redirect to the new object's show page
protected function handle(Customer $customer, string $orderedAt): ActionResult
{
    $order = Order::create([...]);
    return $this->success(__('Order created.'))->object($order);
}

// Simple action — stay on page with a toast
protected function handle(Order $order): ActionResult
{
    Mail::to($order->customer)->send(new OrderConfirmation($order));
    return $this->success(__('Confirmation sent.'));
}

// Delete — redirect somewhere else (no object to go to)
protected function handle(Order $order): ActionResult
{
    $order->delete();
    return $this->success(__('Order deleted.'))->redirectTo(route('orders.index'));
}

// Business logic guard
protected function handle(Order $order): ActionResult
{
    if ($order->isShipped()) {
        return $this->error(__('Cannot approve a shipped order.'));
    }

    $order->approve();
    return $this->success(__('Order approved.'))->object($order);
}

// Workflow
protected function handle(string $source): ActionResult
{
    return $this->success(__('Import started.'))
        ->workflow(ImportOrdersWorkflow::start($source));
}

Redirect Chaining

Chaining ->object() auto-redirects to the object’s show page (via getObjectUrl()). Use ->redirectTo() to override this or navigate elsewhere entirely. The API always ignores redirects.
return $this->success(__('Order duplicated.'))
    ->object($clone)
    ->redirectTo(route('orders.edit', $clone));

How Controllers Interpret Responses

handle() returnsWebAPI
success()->object($o)Redirect to show page + toast{ success, message, data }
success()->redirectTo($url)Redirect to URL + toast{ success, message }
success() message onlyback() + toast{ success, message }
success()->workflow($wf)Store reference, show progress{ success, workflow_id }
error()back() + error toast422 { success: false, message }

Errors

$this->error() returns an ActionResult with success: false. Use it for clean control flow:
if ($order->isShipped()) {
    return $this->error(__('Cannot approve a shipped order.'));
}
For early bailout in deeply nested logic, throw CoreActionException directly — the platform catches it and converts to the same error result.

Preconditions

Preconditions are business-logic guards that run before the user clicks. They determine whether an action button is visible, disabled with an explanation, or fully enabled. The method has access to the subject and all bound params. Use $this->subject() when $subjectClass is declared, and use exact-key accessors for the rest:
protected function preconditions(): PreconditionResult
{
    $order = $this->subject();

    if (! $order->is_ready) {
        return PreconditionResult::hidden();
    }

    return PreconditionResult::check([
        __('Order is in draft status') => $order->status === OrderStatus::DRAFT,
        __('Order has line items') => $order->lines()->exists(),
        __('Customer has a billing address') => $order->customer->hasBillingAddress(),
    ]);
}
There are two outcomes:
  • Hidden — return PreconditionResult::hidden() early. The button is not rendered at all. No message, no trace. Use this for conditions where the action shouldn’t even be visible (e.g. the object is in the wrong state entirely).
  • Disabled — return PreconditionResult::check([...]) with any unmet conditions. The button renders but is disabled with a checklist popover showing exactly what’s required.
If all conditions in check() pass, the button is fully enabled.

Precondition Checklist Popover

When an action has unmet preconditions, hovering over the disabled button reveals a PreconditionChecklist popover. Each condition is listed with a status icon:
  • Green check — condition met
  • Red X — condition not met
This gives users clear, actionable feedback about what needs to happen before the action becomes available. The same popover appears in two places:
  1. CoreActionButton — wraps the button with a hover-triggered popover when preconditions.availability is DISABLED.
  2. CoreActionForm submit button — when rendering a form for an action with preconditions, use CoreActionSubmitButton instead of CoreFormSubmitButton. It disables the submit and shows the same checklist on hover.
The PreconditionChecklist is a standalone component you can also use directly:
import { PreconditionChecklist } from '@/core/components/precondition-checklist';

<PreconditionChecklist conditions={action.preconditions.conditions} />;
Each condition is an object with label (string) and met (boolean). The component renders a compact list — no configuration needed.
Preconditions are optional. If not defined, the action is always available (existing behavior). They replace most uses of hidden(Closure) and disabled(Closure) while providing better UX.

Authorization

Every action must implement authorize(). Use has_permission() for consistent authorization across web and API contexts:
public function authorize(): bool
{
    return has_permission(Permission::MANAGE_ORDERS);
}
For instance-level checks, use $this->subject() when $subjectClass is declared and read any other input explicitly:
public function authorize(): bool
{
    return user()?->can('update', $this->subject()) ?? false;
}

Object Actions

Declare which actions belong to a model by adding objectActions() to your domain object. Access them via Order::actions() (unbound) or $order->actions() (bound to the instance):
app/Models/Order.php
public static function objectActions(): array
{
    return [
        'create' => CreateOrderAction::class,
        'approve' => ApproveOrderAction::class,
        'cancel' => CancelOrderAction::class,
        'send_invoice' => SendInvoiceAction::class,
    ];
}
$order->actions()->only(['approve', 'cancel'])->all();
When instantiating subject-based actions manually, pass the model to make():
ApproveOrderAction::make($order)
CancelOrderAction::make($order)

Object Actions

Full guide: instance vs collection detection, ObjectActionSet API, table integration, and API discovery.

Auto-Modal Forms

When a CoreActionButton is clicked and some visible schema fields still need user input, the platform automatically opens a modal with a form. No custom modal routes or React pages needed.
  • All visible fields already bound → submit directly (respecting confirmation)
  • Any visible field still needs input → open modal with the action’s form
The form is auto-generated from the schema using PropertyCoreFormInput for each property. Any schema property already supplied via bound params is hidden automatically.

Custom Form Component

For complex forms that go beyond what the auto-form can render, provide a custom React component:
protected function form(): ?ActionForm
{
    return $this->component('orders/create-order-form', [
        'customers' => Customer::pluck('name', 'id'),
        'warehouses' => Warehouse::active()->get(),
    ]);
}
The component receives the action data, schema properties, pre-filled params, and your custom props. It uses CoreActionForm for submission. Configure the modal in defaults() using the same fluent syntax as other action properties:
protected function defaults(): void
{
    $this->label(__('Edit'))
        ->icon('Pencil')
        ->modalTitle(__('Edit school'))
        ->submitLabel(__('Save'))
        ->modalSize(ModalSize::LG)
        ->sheet(position: SheetPosition::RIGHT);
}
ModalSize values: SM, MEDIUM, LG, XL, FULL. The frontend maps each to the appropriate Tailwind class. Use modalTitle() to set a custom modal heading. When omitted, the modal header falls back to the action’s label. Use submitLabel() to set a custom label for the submit button in the auto-form modal. When omitted, the submit button falls back to the action’s label. This is useful for distinguishing between the action button label (e.g., “Edit”) and the form submission action (e.g., “Save”).

Workflow Integration

When handle() returns a workflow reference, the platform automatically tracks its state.
protected function handle(string $source): ActionResult
{
    return $this->success(__('Import started.'))
        ->workflow(ImportOrdersWorkflow::start($source));
}
The workflow class owns its own key for deduplication and state lookup:
class ImportOrdersWorkflow extends CoreWorkflow
{
    public static function key(string $source): string
    {
        return "import_orders:{$source}";
    }
}
The key is persisted on the StoredWorkflow model. When the platform serializes the action for the frontend, it resolves the latest workflow by key and includes status URLs in CoreActionData.

CoreActionButton and Workflows

When a CoreActionButton executes an action that returns a workflow, the button automatically switches to a loading/progress state — polling the workflow status URLs and showing progress until completion. This merges the existing WorkflowButton behavior directly into CoreActionButton. No separate component needed.
<CoreActionButton action={refreshHubSpotDealsAction} />
On the initial page load, if the workflow is already running (from a previous click or another user), the button picks up the state from action.stateUrls and resumes showing progress.

CoreActionForm and Workflows

When a CoreActionForm or CoreActionCard submission returns a workflow, the form does not handle the workflow state — it simply completes the submission like any other action. The page is responsible for displaying workflow progress, typically via a WorkflowCard that’s already on the page and reacts to the workflow state independently. This is the right boundary: buttons are fire-and-forget with inline progress, while forms are part of a larger page that manages its own workflow display.

Precognition

Action forms support Laravel Precognition for real-time field-by-field validation. The execute route includes the HandlePrecognitiveRequests middleware, and CoreActionForm enables withPrecognition() automatically. Every action form gets live validation for free — no setup per action.
Not every action executes backend logic. Use these variants for navigation:

LinkCoreAction

Navigate to a URL. Renders as an Inertia <Link> for internal pages or a plain <a> for external URLs.
LinkCoreAction::make(__('View profile'))
    ->href(fn (User $user) => route('users.show', $user))
    ->icon('User');

ModalLinkCoreAction

Open a server-side modal. Use for very custom UIs that don’t fit the action schema pattern.
ModalLinkCoreAction::make(__('Advanced settings'))
    ->url(fn (Order $order) => route('orders.settings', $order))
    ->sheet(position: SheetPosition::RIGHT)
    ->icon('Settings');

Action Dropdowns

Group multiple actions in a dropdown menu:
CoreActionDropdown::make([
    SendInvoiceAction::make($order),
    CancelOrderAction::make($order),
    DeleteOrderAction::make($order),
], label: __('More'));

Passing Actions as Props

Pass actions to the frontend without registering them in the page header:
return AppPage::make('orders/show')
    ->with([
        'approveAction' => ApproveOrderAction::make($order),
    ]);
<CoreActionButton action={approveAction} />

TypeScript Types

Action parameter types are auto-generated alongside object schema types. Run composer generate-ts to update.
import { ActionWithSchema } from '@/core/properties';
import { useCoreActionForm } from '@/core/hooks/use-core-action-form';

const typedAction =
  action as ActionWithSchema<App.Actions.Order.ApproveOrderActionParams>;

const form = useCoreActionForm(typedAction, {
  values: { reason: '' },
});

form.setData('reason', 'Looks good'); // autocomplete works

Testing

Use action_url() to generate the execute URL for an action. When passed an instance with a bound subject, the subject ID is included in the URL path (matching how the platform serializes the action for the frontend):
test('authorized users can approve an order', function () {
    $order = Order::factory()->draft()->create();

    actingAs($admin)
        ->post(action_url(ApproveOrderAction::make($order)))
        ->assertRedirect();

    expect($order->fresh()->status)->toBe(OrderStatus::APPROVED);
});

test('shipped orders cannot be approved', function () {
    $order = Order::factory()->shipped()->create();

    actingAs($admin)
        ->post(action_url(ApproveOrderAction::make($order)))
        ->assertSessionHasErrors();
});

test('unauthorized users cannot approve', function () {
    $order = Order::factory()->draft()->create();

    actingAs($regularUser)
        ->post(action_url(ApproveOrderAction::make($order)))
        ->assertForbidden();
});

// For actions without a subject (e.g. create), pass the class directly
test('authorized users can create an order', function () {
    actingAs($admin)
        ->post(action_url(CreateOrderAction::class), ['customer' => $customer->id])
        ->assertRedirect();
});

// You can also call ->actionUrl() on any action instance
test('action is configured correctly', function () {
    $order = Order::factory()->draft()->create();
    $action = ApproveOrderAction::make($order);

    expect($action->subject())->toBe($order)
        ->and($action->getLabel())->toBe('Approve')
        ->and($action->actionUrl())->toContain('/execute/' . $order->id);
});

URL scheme

Action execute URLs follow the pattern /actions/{key}/execute for unbound actions and /actions/{key}/execute/{id} for subject-bound actions. Both action_url() and ->actionUrl() generate the correct form automatically:
action_url(CreateOrderAction::class)          // /actions/order/create-order/execute
action_url(ApproveOrderAction::make($order))  // /actions/order/approve-order/execute/42

$action = ApproveOrderAction::make($order);
$action->actionUrl()                          // /actions/order/approve-order/execute/42
Sub-namespaces under Inly\Core\Actions become extra path segments after core/, e.g. Inly\Core\Actions\User\ImpersonateUserAction → lookup key core/user/impersonate-user and URL /actions/core/user/impersonate-user/execute.

Reference

Action Properties

PropertyTypeMethodDescription
labelstringlabel()Button label
iconstringicon()Lucide icon name
variantCoreVariantvariant()Button style
descriptionstringdescription()Tooltip / API description
confirmbool|stringconfirm()Show confirmation dialog. Pass a string to set the message.
hiddenbool|Closurehidden()Hide from UI
disabledbool|Closuredisabled()Disable in UI
primaryboolprimary()Show in page header
onlyarrayonly()Inertia partial reload props
modalSizeModalSizemodalSize()Auto-modal width (SM, MEDIUM, LG, XL, FULL)
sheetboolsheet()Open as slideover panel instead of centered dialog
sheetPositionSheetPositionsheet(position:)Sheet side (LEFT, RIGHT)
modalTitlestringmodalTitle()Custom title for auto-modal form (falls back to label)
submitLabelstringsubmitLabel()Custom label for submit button in auto-modal (falls back to label)

Subject Declaration

Property / MethodDescription
$subjectClassprotected static ?string — declare the model class this action operates on
::make($model)Bind a subject at construction: ApproveOrderAction::make($order)
::make($model, $params)Bind a subject plus additional params: UpdateOrderAction::make($order, ['extra' => 'value'])
::make($params)Params-only factory (backward compat / no subject); enforces subject presence if array is non-empty
::make()Unbound template; used internally by tables before setSubject() is called
subject()Returns the bound subject model instance from any action method
setSubject($model)Bind subject after construction; used internally by tables and ObjectActionSet
getSubjectClass()Returns the declared $subjectClass string (or null)
getSubjectKey()Returns the parameter key for the subject (e.g. "order" for Order::class)
url()Returns the execute URL — /execute/{id} form when subject is bound, /execute otherwise

Lifecycle Methods

MethodRequiredDescription
schema()RecommendedParameter schema as Properties
authorize()YesReturn true to allow execution
defaults()NoSet label, icon, variant, confirmation
preconditions()NoBusiness-logic guards with UX feedback
handle()YesExecute the operation
form()NoCustom form component override

Frontend Components

ComponentPurpose
CoreActionButtonRender an action as a button with auto-modal, confirmation, precondition popover, and workflow state
CoreActionFormForm wired to an action — validation, loading, confirmation
CoreActionCardEmbed an action’s form on-page inside a card
CoreActionSchemaFormAuto-rendered form from schema properties
CoreActionSubmitButtonSubmit button with precondition checklist popover support
CoreActionDropdownGroup actions in a dropdown menu
PreconditionChecklistReusable list of pass/fail condition labels

Next Steps

Standard actions

Pre-built base classes for delete, edit, create, restore, and duplicate with consistent defaults and automatic titles.

Action Forms

Build custom forms with CoreActionForm, schema-driven fields, and Precognition.

Table Actions

Row actions, bulk operations, and workflow-powered background processing.