Journey triggers
Natural-language rules that compile to a deterministic IR and auto-enroll cohorts into journeys when a platform event fires.
Journey triggers
A journey trigger turns a plain-English rule into an event-driven enrollment. You write something like "when an opportunity moves to Negotiation, enroll enterprise accounts in the Exec Sponsor journey"; Pact compiles it to a structured intermediate representation (IR), and from then on every matching platform event evaluates a cohort and enrolls the qualifying members into a journey — through the consent gate.
The routes live under /v1/journey-triggers (api/routes/journey_triggers.py) and the
runtime lives in the core.journey_triggers package: an NL compiler, an event listener, an
enrollment engine, and an explainer.
Marketing module + RBAC
All trigger routes require verify_api_key, require_module("marketing"), and the
SEQUENCES_READ / SEQUENCES_WRITE permissions, inheriting the same RBAC as the rest of
the marketing surface. Records are tenant-scoped via get_tenant_id.
Compile first, commit second
The composer calls POST /v1/journey-triggers/parse on debounce to compile NL → IR with
no database write, so you see the IR update as you type. The compiler
(core.journey_triggers.nl_compiler) is deterministic — the same prompt always
produces the same IR, which is what makes triggers reviewable and auditable. It also emits
citations mapping each IR clause back to the phrase in your prompt that produced it.
The IR (TriggerIR) carries:
event_kind— the platform event that fires the trigger.cohort_selector— who to consider when the event fires.event_filter— additional constraints (e.g. destination stage, size band, region).consent_gate— the consent purpose to check (defaultmarketing).enrollment_mode—liveordry_run.journey_id— the journey to enroll qualifying members into.
Recognized events
The event vocabulary is fixed in core.journey_triggers.nl_compiler.EVENT_KINDS (values are
persisted in journey_triggers.ir_json, so they stay stable). Current kinds include:
contract.signed— "contract signed", "deal closed", "closed-won".opp.stage_change— "opportunity stage changes", "moves to stage …".contact.consent_change— "opts in", "opts out", "withdraws consent".account.size_band_change— "becomes enterprise", "moves up market".buyer_group.champion_silent— a champion going quiet on a buyer group.
The compiler also extracts inline filters such as "moves to the Negotiation stage" →
opportunity.stage, and size-band / region qualifiers, attaching a citation for each.
Lifecycle
- 1
Parse
POST /parsecompiles your prompt to IR. No write. - 2
Create draft
POST /v1/journey-triggersstores a draft trigger (statusdraft). You can pass an overriddeniror let the service recompile fromnl_prompt. - 3
Preview
POST /v1/journey-triggers/{id}/previewruns a synthetic event through the enrollment engine and returns the suppression breakdown — nobody is enrolled. - 4
Activate
POST /v1/journey-triggers/{id}/activateflips status toactive;…/pauseflips it back.
What happens when it fires
When the event listener matches an active trigger, the enrollment engine
(core.journey_triggers.enrollment_engine) evaluates each cohort member and runs them
through the consent gate. It records a journey_trigger_runs row per fire with
members_evaluated, members_enrolled, members_suppressed, and a structured
suppression_breakdown_json. Suppression reasons are explicit and countable:
invalid_subject, no_consent:<reason>, already_enrolled:<reason>, no_journey,
journey_invalid.
dry_run mode records without enrolling
A trigger in dry_run (or a preview call) walks the exact same evaluation and
consent-gating path but stops short of writing enrollments — it only records the
suppression breakdown. This is the safe way to validate cohort size and consent posture
before going live.
Explainability
GET /v1/journey-triggers/{id}/runs lists recent fires. For any resulting enrollment,
GET /v1/journey-triggers/explain/{enrollment_id} returns a chain-of-reasoning
(trigger_explainer) that stitches together the run row, the source event, and the trigger
IR — so you can answer "why was this contact enrolled?" down to the event and the clause
that matched.