Automation — Developer Guide
Billable marketplace module (automation 1.9.0). Mirrors Tasks packaging; domain logic lives in a cross-module engine that subscribes to existing domain events.
Backend layout
| Piece | Path |
|---|---|
| Models | app/Models/Automation{Workflow,Branch,Trigger,Condition,Action,Run,Log}.php |
| Enums | AutomationRunStatusEnum, AutomationConditionOperatorEnum, AutomationLogLevelEnum |
| Services | app/Services/Tenant/Automation/* (WorkflowService, AutomationEngine, ConditionEvaluator, ActionRunner, registries, handlers) |
| Related resolver | Support/AutomationRelatedResolver — trigger_assignee, tag/stage by id or name/slug, related-entity labels |
| Bridge | IntegrationEventDispatcher (sole Automation + webhook fan-out; registered in AppServiceProvider) |
| Job | ExecuteAutomationRunJob on queue automations |
| Schedule | automation:dispatch-schedules every minute |
| Controllers | AutomationWorkflowController, AutomationRunController, AutomationCatalogController, AutomationTemplateController |
| Policies | AutomationWorkflowPolicy, AutomationRunPolicy |
| Notification | AutomationWorkflowNotification (NotificationSourceEnum::Workflow) |
| Tests | tests/Feature/Tenant/Automation/*, tests/Unit/Automation/* |
Related context (1.1.1+)
- Trigger payloads already include
entity_type/entity_idplus record fields. Entity-bound actions (assign, tag, note, move stage) use that target automatically. - Config may use semantic
trigger_assignee(resolvesassigned_to→new_assignee_id→host_id) forcreate_task.assigned_to,create_project.assigned_to,assign_user.user_id, andsend_notification.user_ids(arrays supported). - Tags/stages accept numeric ids or matching name/slug.
create_taskappends a related-entity line and posts a note on lead/opportunity/task. Lead follow-up requirescreate_lead_follow_up: true(templates enable it).create_project(1.4.0, moduleprojects) soft-linksopportunity_id/company_id/contact_idfrom the trigger, optionalstarts_in_days/ends_in_days, and notes opportunity/company/contact/lead. Template:opportunity_stage_create_project.send_whatsapp_interactive(1.5.0, modulewhatsapp-cloud) wrapsWhatsAppConversationService::queueInteractive— same conversation/lead resolution assend_whatsapp_template. Config:interactive_type(button|list),body, optionalfooter, and forlistalsoheader/action_button/sections, or forbuttonbuttons(id/title pairs, tokens interpolated per row). Requires the conversation's 24-hour customer service window to be open (same rule as manual interactive send). Template:whatsapp_inbound_quick_replies.generate_quotation(1.5.0, modulequotations) creates aQuotationviaQuotationService::create(default statusdraft). Optional configauto_send(1.6.0) callssend()then emails the bill-to contact/company with PDF. Optionalauto_send_whatsapp(1.7.0) transitions to Sent (shared with email flag) and queues the PDF viaWhatsAppConversationService::queueMediainto an open 24h conversation (resolveconversation_id→lead_id→ bill-to phone). Requiresopportunity_idfrom config or anopportunity.*trigger; soft-links company/contact and notes the opportunity. Template:opportunity_stage_generate_quotation.generate_invoice(1.5.0, moduleinvoices) creates aCustomerInvoiceviaCustomerInvoiceService::create(defaultdraft). Optionalauto_send(1.6.0) andauto_send_whatsapp(1.7.0) mirror quotation (Unpaid + email / WhatsApp PDF). Soft-links quotation/company/contact from the trigger when available.generate_order(1.8.0, modulepurchase-orders) creates aPurchaseOrderviaPurchaseOrderService::create(empty lines). Requiresvendor_idfrom config or a vendor trigger. Optionalauto_send/auto_send_whatsappmark Sent and email/WhatsApp the vendor. Template:manual_generate_purchase_order. Shared helper:Support/AutomationDocumentWhatsAppSender.- Manual Run accepts
payloadwith related entity fields; SPA dialog picks a record for entity-bound triggers.
Activation gate
Workflows are always persisted inactive. Create/update with is_active=true still saves first, then calls activate(). Unwired catalog triggers cannot be activated. Triggers/actions whose module is not entitled also fail activate (and are disabled in the SPA builder via catalog available).
Loop guard & notification source
- While
AutomationContextdepth > 0,AutomationEngine::dispatchEvent()no-ops so action side-effects do not recurse. - Mentions / DMs use
NotificationSourceEnum::MentionandDirectMessage. ReserveWorkflowfor Automation-fired notifications.
Trigger / action registries
Modules publish metadata via AutomationTriggerRegistry and AutomationActionRegistry (singletons with registerDefaults()).
Templates declare required_modules; AutomationTemplateController filters by entitlement. WorkflowService::createFromTemplate rejects missing modules.
Wired trigger families (v1.3.0+): manual/schedule; Leads (including lead.converted); Tasks; Opportunities; Meetings; Invoices; Payments (customer_payment.posted); Credit Notes (customer_credit_note.applied); WhatsApp inbound; Help Desk; Contacts; Quotations; Expenses; Employees; Projects (created / status / assigned); Estimates; Contracts; Purchase Orders; Leave requests; Documents; Knowledge Base articles; Assets.
Creating a project with an assignee can emit both project.created and project.assigned (two intentional domain events). Operators who subscribe both triggers may get two runs for one create — configure one or both deliberately.
Permissions
automation.view | create | update | delete | run | manage_logsRoutes: module:automation then can:automation.*.
Date and time
Schedule evaluation uses workspace timezone from TenantSettingService — see Workspace timezone convention. Daily/weekly/monthly times match within 90 seconds after H:i; duplicate schedule runs for the same workflow are suppressed for 2 minutes.
Outbound webhooks
OutboundWebhookService blocks private/loopback hosts (SSRF). Optional HMAC via AUTOMATION_WEBHOOK_SECRET. Exhausted ExecuteAutomationRunJob retries call failed() and persist run status failed. The job uses a per-run cache lock (so delay continuations wait instead of overlapping) and $timeout = 60 (below Redis retry_after 90). Action delay_seconds is capped at 86400.
Frontend
| Piece | Path |
|---|---|
| Pages | src/pages/automation/* (automation-action-config.tsx pickers + tokens) |
| API | automationService in src/api/services.ts |
| Nav / routes | module: 'automation', /automation* |
| E2E | npm run test:e2e:automation |
Builder disables items where wired=false (“Coming soon”) or available=false (“Module not installed”).
If / else branches (1.9.0)
Workflow conditions still gate the whole run. Shared actions (branch_id null) always run. Optional branches are evaluated in order after that:
- The first non-else branch whose conditions pass runs its actions.
- One otherwise branch (
is_else: true) may be last, has no conditions, and runs when nothing else matched. - If nothing matches and there is no otherwise branch, the run completes after the shared actions and logs
No branch matched. - The chosen action id list is stored on the run payload (
_automation_action_ids) so a delay resumes the same plan. - Branch conditions are not workflow gates. A failing branch does not stop the run from starting.
Template: manual_priority_branch (no extra modules).
Explicit non-goals
Marketing Automation remains deferred. Email auto_send shipped 1.6.0; WhatsApp document auto-send (auto_send_whatsapp) shipped 1.7.0; generate_order (Purchase Order) shipped 1.8.0; if/else branches shipped 1.9.0. Standalone send_whatsapp_document action and Sales Orders module stay out of scope. create_project shipped in Automation 1.4.0; send_whatsapp_interactive / generate_quotation / generate_invoice shipped in Automation 1.5.0. WhatsApp Cloud trigger whatsapp.message_received and action send_whatsapp_template shipped in Automation 1.1.0 / WhatsApp Cloud 1.2.0. Help Desk triggers shipped with Help Desk 1.3.0; SLA escalate template with Help Desk 1.9.0.