Validation
An action is called from places that never saw an HTTP request: a console command, a scheduled task, a queue worker, a test. A $rules array moves the guarantee about its inputs from the caller into the action, so it holds wherever the call came from.
class SendWelcomeEmail extends Action { protected array $rules = [ 'email' => 'required|email', 'subject' => 'required|string|max:120', ]; public function handle(string $email, string $subject): void { Mail::to($email)->send(new WelcomeMail($subject)); } }
The keys are the parameter names of handle(), not the keys of whatever you passed in. That matters because the same action can be called positionally:
SendWelcomeEmail::create()->run('not-an-email', 'Welcome');
The arguments are resolved to email and subject first, and the rules are applied to the result, so this fails validation exactly like the array form would.
When it runs
Validation happens inside run(), after arguments are resolved and after handle() defaults are filled in, and before handle() is entered. A rule therefore sees the value the method would actually have received, including a default it never had to be given.
Failure throws Illuminate\Validation\ValidationException. In an HTTP context Laravel turns that into a 422 with the usual error bag, so an action called straight from a controller behaves like a form request without one. Everywhere else it is an ordinary exception you can catch.
For a queued action, all of this happens on the worker, because that is where run() is called. Dispatching never validates. See Asynchronous actions.
Why it is worth setting even for internal actions
Arguments that are not supplied are resolved to null rather than raising an error. Without rules, a forgotten argument reaches handle() and turns into a TypeError deep inside the method, or worse, slips through a nullable parameter and does something quiet and wrong. Rules turn that into a named failure before any work starts:
protected array $rules = [ 'user_id' => 'required|integer|exists:users,id', 'amount' => 'required|numeric|min:0.01', ];
Validating an attribute bag
When handle() takes a single array parameter, the whole array is bound to that one parameter, so the validated data is nested one level deep under the parameter name. Rules use dot notation to reach inside it:
class CreateInvoice extends Action { protected array $rules = [ 'attributes.concept' => 'required|string', 'attributes.amount' => 'required|numeric', ]; public function handle(array $attributes) { /* ... */ } }
Writing 'concept' => 'required' there would validate a key that does not exist at the top level, and pass silently when the concept is missing. It is the one place where the attribute bag rule and the rules array have to agree with each other.
What is not validated
with() sets properties on the action rather than arguments for handle(), and properties are not part of the validated payload. If a value has to be checked, take it as a handle() parameter. If it is configuration of the action rather than data, with() is the right place and validation is not what you want there anyway.
An empty or absent $rules array means no validation, which is the default for a generated action.