Trigger / Workflow Automation Engine
Overview
The trigger engine is the heart of the entire automation system. The idea is simple: "if event X
happens to a customer and they satisfy condition Y, do Z." These rules live in the trigger_rule
table and can be attached to a workflow.
The trigger-worker profile runs consumers on eight queues, one per source event type, but
they all converge on the same pipeline: filter rules → check dedup and cooldown → evaluate
conditions → dispatch the action, or schedule it for later.
Business Flow
The eight source types
| Queue | Source type | Where the event comes from |
|---|---|---|
attribute_change | attribute_change, batched | pg-listener via pg_notify('attribute_changed') and the import path |
attribute_change_realtime | attribute_change, per user | the realtime path |
audience_membership_trigger | audience_membership | the audience consumer, on member add or removal |
friend_track_event_trigger | friend_track_event | pg-listener via friend_track_event_inserted |
form_submitted_trigger | form_submitted | pg-listener, when client-api saves a form |
campaign_click_trigger | campaign_click | pg-listener via a trigger on the tracking_log table |
scheduled_trigger_fire | scheduled | the cron scanner on the cron-scheduler profile |
scheduled_action_fire | — | the cron scanner, for actions that have come due |
Pipeline for a single message
- Source-type gate —
HasActiveSourceType(lineOaId, organizationId, sourceType)consults a cache (60-second TTL) to check whether the OA has any enabled rule of this type. If not, the message is acknowledged immediately without touching the database, which stops event floods from doing pointless work. - Load rules —
getTriggerRules(60-second cache), then filter down to rules matching both the source type and the target, for example requiringsourceConfig.audienceIdto match the audience that changed. isKeyRelevant— for attribute changes, skip immediately if the changed key has nothing to do with the rule's conditions.- Dedup and cooldown —
shouldSkipByDedupuses Redis to stop the same rule from firing repeatedly for the same user, honouringfrequency(for example, once ever) andcooldownSeconds. - Evaluate conditions —
evaluateConditionsandevaluateCondition- a full set of comparison operators is supported, including day-count date comparisons where
roundDiffDaysmirrors JavaScript'sMath.roundbehaviour so results match the legacy system - split tests are supported through
evaluateSplitTest, which partitions users deterministically from their userId
- a full set of comparison operators is supported, including day-count date comparisons where
- Execute the action —
executeAction(rule, user, triggerDepth)- if
actionConfig.delayis set, the action is not run immediately;ScheduleActionrecords it into thescheduled_actiontable instead (see Delayed / Scheduled Actions) - otherwise, an
ActionExecutePayloadis published ontoaction_execute, or ontoweb_request_executewhenactionType = web_request - the constant
maxTriggerDepth = 5guards against an action that triggers further triggers and spirals into an infinite loop
- if
- Write the log — record the rule id, user, triggerOn, status, and action type/config into
trigger_log. - Most handlers always return nil because the service handles its own errors and logging; only a malformed payload reaches the DLQ.
scheduled_trigger_fire
The cron finds rules with source_type='scheduled' that have come due, evaluating the schedule
per rule in the Asia/Bangkok timezone, and publishes them onto this queue. The consumer then
expands each into per-user work in batches of 500 (scheduledFireBatchSize) and runs them
through the same pipeline.
Key Files & Functions
internal/trigger/consumer.go—Consumer.Register()binds all eight queues, with the handlersHandleAttributeChange,HandleRealtimeAttributeChange,HandleAudienceMembershipTrigger,HandleFriendTrackEventTrigger,HandleFormSubmittedTrigger,HandleCampaignClickTrigger,HandleScheduledTriggerFire, andHandleScheduledActionFireinternal/trigger/service.goTriggerService.EvaluateTriggers(),EvaluateTriggersForBatch(),processAttributeRule()HasActiveSourceType(),getTriggerRules(),shouldSkipByDedup(),isKeyRelevant()evaluateCondition(),evaluateConditions(),evaluateSplitTest(),getUserValue()executeAction(),getLineUser(),mapLineUser()- the constants
cacheTTLSecs = 60,maxTriggerDepth = 5,scheduledFireBatchSize = 500
internal/trigger/service_events.goEvaluateAudienceTriggers(),EvaluateFriendTrackTriggers(),EvaluateFormSubmittedTriggers(),EvaluateCampaignClickTriggers()ProcessScheduledTriggerFire(),ProcessScheduledActionFire(),handleScheduledActionExpired()
internal/trigger/repository.go—TriggerRuleRepositoryandTriggerLogRepository.LogTrigger()internal/payloads/payloads.go—ActionExecutePayload, the cross-domain contractcmd/worker/main.go—runTriggerWorker(), which cross-wiresTriggerServiceandScheduledActionService- Queues: consumes all eight queues above (profile
trigger-worker) and publishes toaction_executeandweb_request_execute
Connections to Other Services
- Receives jobs from: pg-listener (five channels), audience processing, line user CSV import,
and the cron scanner on the
cron-schedulerprofile - Tables:
trigger_rule(the rules themselves),trigger_log(firing records),line_user(the user data conditions are evaluated against),scheduled_action(delayed actions), andworkflow(rule grouping) - Redis: caches for rules and the source-type gate (60-second TTL), plus dedup and cooldown keys per (rule, user) pair
- RabbitMQ: publishes onward to Workflow Action Execution and External API Calls from Workflows
- ENV toggles:
ENABLE_TRIGGER_EVALUATIONandENABLE_TRIGGER_SOURCE_TYPE_CACHE - CMS side: the cms-api-go workflow and trigger-rule domains are what create and edit these rules