PPactDocs
CRM

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 (default marketing).
  • enrollment_modelive or dry_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. 1

    Parse

    POST /parse compiles your prompt to IR. No write.
  2. 2

    Create draft

    POST /v1/journey-triggers stores a draft trigger (status draft). You can pass an overridden ir or let the service recompile from nl_prompt.
  3. 3

    Preview

    POST /v1/journey-triggers/{id}/preview runs a synthetic event through the enrollment engine and returns the suppression breakdown — nobody is enrolled.
  4. 4

    Activate

    POST /v1/journey-triggers/{id}/activate flips status to active; …/pause flips 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.