A trigger watches for an event and decides whether to start (or resume) an automation. The TriggerRegistry registers a handler per type. When an event reaches AutomationEventListener, the registry finds matching triggers, each runs shouldTrigger and extractContext, and the runner kicks off the workflow.
This page lists every trigger that ships with AppEngine, the upstream event types it listens to, and the variables it leaves on context.variables for downstream steps.
Built-in triggers
| Field | Type | Description |
|---|---|---|
| scheduled_time | time-based | Fires on cron, fixed interval, or one-shot at a future timestamp. Listens to |
| contact_created | CRM event | Fires shortly after a contact is created (within 5 minutes). Listens to |
| email_opened | Broadcast event | Fires on email-tracking open events. Listens to |
| email_clicked | Broadcast event | Fires on link-click events. Listens to |
| appointment_booked | Calendar event | Listens to |
| page_visited | Tracking event | Listens to |
| contact_tagged | CRM event | Listens to |
| database_change | Repository event | Generic CDC trigger. Listens to |
| call_incoming | Phone event | Inbound voice call. Used to drive IVR flows. |
| call_missed | Phone event | Missed call. Pair with |
| sms_received | Phone event | Inbound SMS. Pair with |
Payload shapes
What a step sees on context.variables depends on the trigger.
scheduled_time
{
"triggerType": "scheduled_time",
"scheduledAt": "2026-04-25T09:00:00Z",
"currentTime": "2026-04-25T09:00:01Z",
"scheduleId": "sched_abc",
"cronExpression": "0 9 * * MON"
}
contact_created
{
"contact": {
"id": "c-123",
"email": "[email protected]",
"firstName": "Alice",
"tags": ["newsletter"],
"source": "website",
"createdAt": "2026-04-25T..."
},
"triggerResult": {
"triggerType": "contact_created",
"contactId": "c-123",
"contactEmail": "[email protected]",
"contactName": "Alice",
"source": "website"
}
}
email_opened / email_clicked
{
"emailEvent": {
"id": "evt-1",
"messageId": "msg-1",
"eventType": "opened",
"timestamp": "2026-04-25T...",
"url": "https://...",
"userAgent": "..."
},
"contact": { "id": "c-123", "email": "..." }
}
email_clicked adds emailEvent.url (the clicked link).
appointment_booked
{
"appointment": {
"id": "appt-1",
"startsAt": "2026-04-30T15:00:00Z",
"endsAt": "2026-04-30T15:30:00Z",
"attendees": ["[email protected]"],
"bookedAt": "2026-04-25T..."
},
"contact": { "id": "c-123" }
}
page_visited
{
"pageVisit": {
"url": "https://example.com/pricing",
"referrer": "https://google.com",
"visitedAt": "2026-04-25T..."
},
"contact": { "id": "c-123" }
}
contact_tagged
{
"tagEvent": {
"contactId": "c-123",
"tag": "vip",
"taggedAt": "2026-04-25T..."
}
}
database_change
The most flexible — fires on every data_* repository event. Filter narrowly to avoid runaway evaluations.
{
"datatype": "lead",
"operation": "updated",
"record": { /* the new BaseModel */ },
"previous": { /* the prior version, on update */ },
"triggerConfig": { "datatype": "lead", "operation": "updated" }
}
Without a datatype filter, the trigger evaluates on every write across the org. Always set config.datatype to the collection you care about and pin config.operation if possible.
Recency window
Most contact, email, page-visit, and appointment triggers reject events older than five minutes. This protects against late-arriving webhooks accidentally re-firing automations on stale data. If you replay events deliberately (e.g. backfilling), use POST /automation/:automationId/execute with the variables you want instead.
Circular prevention
database_change runs each event through TriggerGuardService first. By default the guard blocks:
- An automation triggering itself (
preventSelfTrigger). - More than 5 nested executions (
maxDepth). - A re-run against the same record within 1 second (
cooldownMs).
Override per-flow via settings.circularPrevention. See flow builder.