Automations
The automations module (app-modules/automations) replaces hard-coded “what happens around”
behaviour, such as follow-up tasks, tags and notifications, with rules the business can change.
Pipeline modules stay correct and announce events, and the business decides what follows. Staff
build rules at System > Automations: see automations.
A rule is When (a trigger), If (conditions, all must hold) and Then (actions, in order), straight away or after a wait.
It knows nothing about sales
Section titled “It knows nothing about sales”Other modules register what rules can be built from, at boot, through the Automations facade
(Registry\AutomationRegistry, a singleton). Two do so today: Sales\Automations\SalesAutomations
and Catalog\Automations\CatalogAutomations.
| Call | Registers |
|---|---|
Automations::subject($class, $label, $fields) |
A record type rules can be about, with fields for conditions |
Automations::trigger($key, $event, $label, $subject, $subjectOf, $dataOf, $fields) |
An event, and how to get the record from it |
Automations::action(Action) |
Something a rule can do |
Automations::notifies($class, NotifyTarget) |
What Notify staff knows about a record type |
$key values, on triggers and actions alike, are stored on rules. Never rename one.
Fields are Fields\ChoiceField and Fields\NumberField. A field’s closure gets (Model $subject, array $eventData) and is read when the rule runs, so conditions see the record as it is then.
How a rule runs
Section titled “How a rule runs”- An event fires. Registering a trigger attaches
Running\QueueRulesForEventto its event class. Sales events dispatch after commit. QueueRulesForEventdoes nothing if automations are switched off (Models\AutomationSettings) or paused. For each trigger whose record resolves, it queues oneJobs\RunAutomationRuleper active rule, bysort_order, or stores anAutomationPendingRunfor a rule that waits.- The job re-checks that the rule is on and the record exists, then the conditions
(
Running\ConditionEvaluator). The first that fails is the skip reason. - The actions run in one transaction, so a failure undoes them all. The job retries 3 times, 30 seconds apart.
- An
AutomationRunrecords every outcome: ran, skipped or failed.
Invariants to keep in mind:
- Loops are capped. An action can fire events that start other rules. The depth travels in
Laravel’s Context, so it survives the queue, and at
AutomationRegistry::MAX_DEPTH(5) no more rules are queued and a warning is logged. - Bulk work pauses them. Wrap imports, seeders and data fixes in
Automations::paused(fn () => ...). - Attribution. While actions run,
Automations::currentRule()gives the rule, and sales’ timeline shows “Rule: name” in place of a person.
Run-due: waiting and repeating
Section titled “Run-due: waiting and repeating”automations:run-due (Console\Commands\RunDueAutomations) is scheduled every minute without
overlapping, by AutomationsServiceProvider. Production needs the scheduler’s cron entry (queues
and scheduling).
It deletes each due AutomationPendingRun first, so an overlapping run can’t start it too, then
queues RunAutomationRule, which checks conditions as things are then. So a payment reminder
stops by itself once the invoice is paid.
- There is one pending run per rule and record. A new trigger replaces what waits and restarts the clock.
- With automations switched off, due runs are dropped, not caught up later.
- A repeating rule (
repeat_every,repeat_unit,repeat_times) stores its next run after each success. A run skipped or failed queues nothing, which ends the repeats. - Changing a rule’s wait moves its waiting first runs. Deleting a rule deletes them.
model:prune removes old AutomationRun rows daily, by the retention days in
Settings\AutomationsSettings (under Housekeeping on Settings).
Adding a trigger
Section titled “Adding a trigger”Suppose sales gains a QuoteExpired event.
-
The event. Extend
SalesDocumentEvent, fired from the right place. A sales event then dispatches after commit, and carriesactorId. -
Register it in the owning module’s registration class, called from its service provider’s
boot():Automations::trigger('quote.expired', QuoteExpired::class, 'Quote expired', Quote::class, $document);$documentinSalesAutomationsreturns the event’s document when it is a quote, else null. A closure can return null to say the event isn’t this trigger, so one event can serve several (NoteAddedon a quote is notNoteAddedon a customer). -
Event data, if a condition needs it. Pass
$dataOffor plain values the queued job needs, and$fieldsthat read them. Seecatalog.product_changed_by_feedinCatalogAutomations. -
A new record type also needs
Automations::subject(), andAutomations::notifies()for Notify staff to link to it and offer placeholders.
Don’t put a listener in a Listeners/ folder for this: QueueRulesForEvent is deliberately
elsewhere so modular’s event discovery doesn’t also register it.
Adding an action
Section titled “Adding an action”Implement Actions\Action and register it. The sales AddTag action is a short example:
class AddTag implements Action{ public function key(): string { return 'add_tag'; } // stored on rules; never rename
public function label(): string { return 'Add tag'; }
public function supports(string $subject): bool { return in_array($subject, [Quote::class, Customer::class], true); }
public function schema(string $subject): array // its settings form on the rule screen { return [Select::make('tag_id')->label('Tag')->options(/* ... */)->required()]; }
public function execute(Model $subject, array $settings, RunContext $context): string { $tag = Tag::query()->findOrFail($settings['tag_id'] ?? null);
return $subject->addTag($tag) ? "Tagged {$tag->name}." : "Already tagged {$tag->name}."; }}execute() returns what it did, for the run log, and runs inside the rule’s transaction. Throw to
undo the rule. Then Automations::action(new AddTag).
The rule screen builds its Builder block from schema(), with one block per action that supports
the trigger’s record type, so an action needs no Filament code of its own. Settings stored on rules
must not change meaning later.
Starting rules and tests
Section titled “Starting rules and tests”SalesAutomationRulesSeeder seeds the starting rules, switched off.
tests/Feature/AutomationEngineTest.php runs the engine against a throwaway Gadget record, so it
depends on nothing from sales. Tests use the sync queue, so a rule runs inline. To test a new
action, create a rule with it and fire the event.
AutomationRulePolicy, AutomationRunPolicy and AutomationPendingRunPolicy all check
automations.manage.