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:
| Type | Command |
|---|---|
| Edit | make:core-action --object=Order --type=edit |
| Create | make:core-action --object=Order --type=create |
| Delete | make:core-action --object=Order --type=delete |
| Restore | make:core-action --object=Order --type=restore |
| Duplicate | make:core-action --object=Order --type=duplicate |
--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:
app/Actions/Order/ApproveOrderAction.php
2. Use in a Controller
app/Http/Controllers/OrderController.php
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
Schema
Theschema() method declares an action’s parameters as Properties. This single definition drives validation rules, form rendering, API documentation, and TypeScript type generation.
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:
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 withonly(), exclude with except(), and add action-specific properties with merge():
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():
make():
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 introspectionmake($model)/make($model, $params)— bind subject at construction time with type enforcementsetSubject($model)— used internally by tables andObjectActionSetfor per-row bindingresolveSubjectParamName()— derives from$subjectClass(or asetSubject()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:
Values, Defaults, and Bound Params
Action forms have three separate concepts:- Bound params: values passed via
make(...),params(...), or frontendparams/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(...).
- Bound params
- Programmatic or subject-derived values
- Defaults
->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:
->default(...) when you want a fallback only if the property does not already have a value:
- 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 definesrules() 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:
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:
| Method | Returns | Description |
|---|---|---|
subject() | Model | The declared $subjectClass instance |
get(name) | mixed | Raw resolved value |
date(name) | Carbon | Parsed as Carbon instance |
integer(name) | int | Cast to integer |
float(name) | float | Cast to float |
boolean(name) | bool | Cast to boolean |
string(name) | Stringable | Cast to Stringable |
enum(name, Enum::class) | typed enum | Cast to enum |
collect(name) | Collection | Cast to collection |
validated() | array | Full resolved parameter bag |
safe() | ValidatedInput | For 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(), andform()only have access to the bound subject and bound params. - Execution phase:
handle()has the validated submission available through$this->get(), the typed accessors, andvalidated().
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
$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
Everyhandle() method returns an ActionResult. Two builder methods set the outcome, and chainable methods attach what happens next:
Common Patterns
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.
How Controllers Interpret Responses
handle() returns | Web | API |
|---|---|---|
success()->object($o) | Redirect to show page + toast | { success, message, data } |
success()->redirectTo($url) | Redirect to URL + toast | { success, message } |
success() message only | back() + toast | { success, message } |
success()->workflow($wf) | Store reference, show progress | { success, workflow_id } |
error() | back() + error toast | 422 { success: false, message } |
Errors
$this->error() returns an ActionResult with success: false. Use it for clean control flow:
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:
- 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.
check() pass, the button is fully enabled.
Precondition Checklist Popover
When an action has unmet preconditions, hovering over the disabled button reveals aPreconditionChecklist popover. Each condition is listed with a status icon:
- Green check — condition met
- Red X — condition not met
CoreActionButton— wraps the button with a hover-triggered popover whenpreconditions.availabilityisDISABLED.CoreActionFormsubmit button — when rendering a form for an action with preconditions, useCoreActionSubmitButtoninstead ofCoreFormSubmitButton. It disables the submit and shows the same checklist on hover.
PreconditionChecklist is a standalone component you can also use directly:
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 implementauthorize(). Use has_permission() for consistent authorization across web and API contexts:
$this->subject() when $subjectClass is declared and read any other input explicitly:
Object Actions
Declare which actions belong to a model by addingobjectActions() to your domain object. Access them via Order::actions() (unbound) or $order->actions() (bound to the instance):
app/Models/Order.php
make():
Object Actions
Full guide: instance vs collection detection, ObjectActionSet API, table integration, and API discovery.
Auto-Modal Forms
When aCoreActionButton 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
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:CoreActionForm for submission.
Modal Preferences
Configure the modal indefaults() using the same fluent syntax as other action properties:
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
Whenhandle() returns a workflow reference, the platform automatically tracks its state.
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 aCoreActionButton 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.
action.stateUrls and resumes showing progress.
CoreActionForm and Workflows
When aCoreActionForm 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 theHandlePrecognitiveRequests middleware, and CoreActionForm enables withPrecognition() automatically.
Every action form gets live validation for free — no setup per action.
Link and Modal Link Actions
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.
ModalLinkCoreAction
Open a server-side modal. Use for very custom UIs that don’t fit the action schema pattern.Action Dropdowns
Group multiple actions in a dropdown menu:Passing Actions as Props
Pass actions to the frontend without registering them in the page header:TypeScript Types
Action parameter types are auto-generated alongside object schema types. Runcomposer generate-ts to update.
Testing
Useaction_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):
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:
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
| Property | Type | Method | Description |
|---|---|---|---|
label | string | label() | Button label |
icon | string | icon() | Lucide icon name |
variant | CoreVariant | variant() | Button style |
description | string | description() | Tooltip / API description |
confirm | bool|string | confirm() | Show confirmation dialog. Pass a string to set the message. |
hidden | bool|Closure | hidden() | Hide from UI |
disabled | bool|Closure | disabled() | Disable in UI |
primary | bool | primary() | Show in page header |
only | array | only() | Inertia partial reload props |
modalSize | ModalSize | modalSize() | Auto-modal width (SM, MEDIUM, LG, XL, FULL) |
sheet | bool | sheet() | Open as slideover panel instead of centered dialog |
sheetPosition | SheetPosition | sheet(position:) | Sheet side (LEFT, RIGHT) |
modalTitle | string | modalTitle() | Custom title for auto-modal form (falls back to label) |
submitLabel | string | submitLabel() | Custom label for submit button in auto-modal (falls back to label) |
Subject Declaration
| Property / Method | Description |
|---|---|
$subjectClass | protected 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
| Method | Required | Description |
|---|---|---|
schema() | Recommended | Parameter schema as Properties |
authorize() | Yes | Return true to allow execution |
defaults() | No | Set label, icon, variant, confirmation |
preconditions() | No | Business-logic guards with UX feedback |
handle() | Yes | Execute the operation |
form() | No | Custom form component override |
Frontend Components
| Component | Purpose |
|---|---|
CoreActionButton | Render an action as a button with auto-modal, confirmation, precondition popover, and workflow state |
CoreActionForm | Form wired to an action — validation, loading, confirmation |
CoreActionCard | Embed an action’s form on-page inside a card |
CoreActionSchemaForm | Auto-rendered form from schema properties |
CoreActionSubmitButton | Submit button with precondition checklist popover support |
CoreActionDropdown | Group actions in a dropdown menu |
PreconditionChecklist | Reusable 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.