Skip to content

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 ​

PiecePath
Modelsapp/Models/Automation{Workflow,Branch,Trigger,Condition,Action,Run,Log}.php
EnumsAutomationRunStatusEnum, AutomationConditionOperatorEnum, AutomationLogLevelEnum
Servicesapp/Services/Tenant/Automation/* (WorkflowService, AutomationEngine, ConditionEvaluator, ActionRunner, registries, handlers)
Related resolverSupport/AutomationRelatedResolver — trigger_assignee, tag/stage by id or name/slug, related-entity labels
BridgeIntegrationEventDispatcher (sole Automation + webhook fan-out; registered in AppServiceProvider)
JobExecuteAutomationRunJob on queue automations
Scheduleautomation:dispatch-schedules every minute
ControllersAutomationWorkflowController, AutomationRunController, AutomationCatalogController, AutomationTemplateController
PoliciesAutomationWorkflowPolicy, AutomationRunPolicy
NotificationAutomationWorkflowNotification (NotificationSourceEnum::Workflow)
Teststests/Feature/Tenant/Automation/*, tests/Unit/Automation/*
  • Trigger payloads already include entity_type / entity_id plus record fields. Entity-bound actions (assign, tag, note, move stage) use that target automatically.
  • Config may use semantic trigger_assignee (resolves assigned_to → new_assignee_id → host_id) for create_task.assigned_to, create_project.assigned_to, assign_user.user_id, and send_notification.user_ids (arrays supported).
  • Tags/stages accept numeric ids or matching name/slug.
  • create_task appends a related-entity line and posts a note on lead/opportunity/task. Lead follow-up requires create_lead_follow_up: true (templates enable it).
  • create_project (1.4.0, module projects) soft-links opportunity_id / company_id / contact_id from the trigger, optional starts_in_days / ends_in_days, and notes opportunity/company/contact/lead. Template: opportunity_stage_create_project.
  • send_whatsapp_interactive (1.5.0, module whatsapp-cloud) wraps WhatsAppConversationService::queueInteractive — same conversation/lead resolution as send_whatsapp_template. Config: interactive_type (button|list), body, optional footer, and for list also header / action_button / sections, or for button buttons (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, module quotations) creates a Quotation via QuotationService::create (default status draft). Optional config auto_send (1.6.0) calls send() then emails the bill-to contact/company with PDF. Optional auto_send_whatsapp (1.7.0) transitions to Sent (shared with email flag) and queues the PDF via WhatsAppConversationService::queueMedia into an open 24h conversation (resolve conversation_id → lead_id → bill-to phone). Requires opportunity_id from config or an opportunity.* trigger; soft-links company/contact and notes the opportunity. Template: opportunity_stage_generate_quotation.
  • generate_invoice (1.5.0, module invoices) creates a CustomerInvoice via CustomerInvoiceService::create (default draft). Optional auto_send (1.6.0) and auto_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, module purchase-orders) creates a PurchaseOrder via PurchaseOrderService::create (empty lines). Requires vendor_id from config or a vendor trigger. Optional auto_send / auto_send_whatsapp mark Sent and email/WhatsApp the vendor. Template: manual_generate_purchase_order. Shared helper: Support/AutomationDocumentWhatsAppSender.
  • Manual Run accepts payload with 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 AutomationContext depth > 0, AutomationEngine::dispatchEvent() no-ops so action side-effects do not recurse.
  • Mentions / DMs use NotificationSourceEnum::Mention and DirectMessage. Reserve Workflow for 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_logs

Routes: 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 ​

PiecePath
Pagessrc/pages/automation/* (automation-action-config.tsx pickers + tokens)
APIautomationService in src/api/services.ts
Nav / routesmodule: 'automation', /automation*
E2Enpm 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.

Official documentation for the EloSync SaaS Platform.