Skip to main content

Journey enrollment fan-out

The campaign lifecycle page names the difference between blast, drip, and journey in one sentence: a journey’s audience is a stream, not a fixed set. This page is the other side of that sentence — the machinery that decides who enters the stream, when, and under what limits. Three parallel enrollment routers watch different trigger sources, and every one of them funnels into the same guarded entry chain before a contact becomes an enrollment. Read this before you build an automation against journey statuses, set a re-entry policy, or wonder why a contact you expected to enroll did not.

What enters a journey

A contact enters a journey through exactly one of three trigger sources, each watched by its own router: One more path deliberately bypasses this page’s model: the single-contact enrollment endpoint behind the contact-360 “enroll” action. It targets one contact you picked by hand, so it skips the matching layer but still rides the same guarded entry chain. All three routers resolve their trigger set the same way: scan the tenant’s running journeys for a trigger node whose type and config line up, cache that set per tenant for 60 seconds, then evaluate each incoming signal in memory. Only running journeys are ever resolved — a draft, paused, or completed journey matches nothing.

Trigger sources

Each router matches on a different predicate against the journey’s trigger node configuration: These routers are deliberately decoupled. The inbound-reply router runs in-process, right beside the inbound message handler that already persists the reply. The segment routers fire from the segment-refresh scheduler tick. The CDP event router receives events from the CDP ingest fan-out. None of them calls another; they only share the entry chain described next.

The offboard precheck and re-entry guard

Before any router enrolls a contact, the same chain runs — this is the fan-out’s single point of consistency:
  1. Offboard precheck. The contact row is read once. A contact who is soft-deleted, blocked, flagged do-not-contact, or globally opted out is dropped before any enrollment write — an inbound reply from an opted-out number can never re-subscribe that contact to outbound journey sends. This precheck is a read-path liveness gate: it mirrors what the send-side suppression layer enforces at dispatch time, but catches the exclusion at the entry boundary instead of enrolling-then-suppressing.
  2. Active-enrollment guard. One batched query resolves which of the matched journeys already hold an active enrollment for this contact (statuses outside the terminal set). A mid-journey contact is never double-enrolled; a partial unique index at the database layer is the structural backstop for the same invariant, so a race between router and writer still ends with one enrollment.
  3. Re-entry policy. The entry executor consults the journey’s re_entry_policy against the contact’s most recent terminal enrollment:
    • never (default) — a contact who already finished or exited the journey cannot re-enter.
    • always — any new trigger re-enrolls them.
    • after_cooldown_days — re-entry allowed once re_entry_cooldown_days have passed since the last terminal enrollment. An operator-cancelled enrollment is intentionally not treated as a lock-out; the cancel was about that run, not the journey as a class.
Tenant-owned controls only: the offboard flags are the contact record’s own soft-delete/block/DNC/global-opt-out fields — nothing here consults a provider-side or country-level suppression list.

Entry rate limiting

A segment trigger can match thousands of contacts on a single tick when a segment definition shifts or a backfill lands. Without a pace knob, all of them enroll in the same minute and the first send node slams the messaging provider’s rate ceiling. entry_rate_limit_per_hour on the journey definition is that knob. When set to a positive integer, every entry-side router counts the campaign’s enrollments created in the rolling last 60 minutes before dispatching:
  • If the count meets or exceeds the limit, the enrollment is skipped with a structured log line, not queued. The contact is simply not re-evaluated until the next router cycle — the next event, the next segment tick — at which point older enrollments have aged out of the window and drainage resumes.
  • The count excludes cancelled enrollments (an operator’s over-enrollment correction should not burn the hourly budget) and includes completed ones (a contact who raced through the journey still represents real entry pressure).
  • A malformed value — non-integer, zero, negative — collapses to “no cap” rather than enforcing a surprising limit. The default is no cap, so existing journeys change nothing until you set the field.
The rate limit is a marketer-pace control, not a safety guard. The channel-level deliverability ceiling stays where it always was: the provider rate limiter and the messaging frequency-cap layer on the send path.

Node state machine

An enrollment is a row keyed to the campaign and contact, sitting in active until it lands in a terminal status (completed, exited, or cancelled). The graph it moves through is the journey definition — nodes and edges — and the executor walks it per contact:
  • Wait nodes. waitDelay parks the enrollment with a resume_at timestamp and returns — the scheduler re-enters the graph when the time passes. waitForEvent parks it stamped with the event name it waits for; the CDP event router’s waiter-locator matches a later event against those stamps and resumes the contact at that node. waitUntilDate parks until an absolute date, optionally read off a contact field. All three leave the enrollment active, so a waiting contact still counts against the active-enrollment guard above.
  • Branch nodes. condition evaluates contact fields against comparison operators and picks an edge. abSplit assigns the contact to a variant arm. channelPreferenceBranch and smartChannel pick the send channel from the contact’s preferences or engagement signal.
  • Goal and exit. exitJourney moves the enrollment to a terminal status mid-graph — this is how “stop messaging them once they convert” is expressed. journeyEnd marks the natural end of a path. Goal attainment (a purchase, a reply, a conversion event) routes a contact to an exit node and stops their remaining steps.
  • Send and side-effect nodes. sendMessage, voiceCall, ussdPush, webhook, list and tag mutations, attribute updates, AI-agent handoffs — each executes and follows its outgoing edges.
Cycle protection: one traversal carries a visited set; a node revisited within the same traversal halts with an error. A resume is a fresh traversal by design — re-entering the same send node after a day-long wait is a feature, not a cycle. The executor’s single entry point for everything described here is the resume-function the routers call with the matched first node: every router ends at the same call, so the re-entry policy, the abort-signal contract (a paused or cancelled journey stops mid-traversal rather than finishing queued sends), and the completion check behave identically no matter which router fired.

Failure semantics

The routers degrade toward no enrollment, never toward a partial one:
  • A trigger-resolution failure (tenant DB blip, malformed journey definition) returns an empty match set — the signal is dropped, nothing is enrolled, the mistake is a missed entry rather than a wrong one.
  • The re-entry guard and rate-limit window counts degrade open on a DB error — a flaky read must not silently drop legitimate enrollments; the database-level unique index on active enrollments remains the backstop against duplicates.
  • A failed enrollment dispatch is isolated per journey: one matched journey’s throw is captured to monitoring with a failure counter, and the remaining matched journeys for the same signal still enroll. Your contact is never half-enrolled — either the enrollment landed or it did not, and the untouched contact carries no partial state.
Operationally, this means a router outage costs you delayed enrollments, not corrupt ones — and the failure counters (inbound_reply_enrollment_failed_total, the segment-router equivalents) are the early warning that a systemic fault is hiding behind what looks like a slow trickle.

Worked example: “STOP” to a goal

A contact on your list replies STOP to an SMS:
  1. The inbound message lands and persists in the conversation.
  2. The inbound-reply router resolves the tenant’s running journeys with inbound_reply triggers (cached 60s) and finds your “Winback on STOP-intent” journey, whose keyword config is keywords: ["stop"], matchMode: "contains", channel sms. The reply body matches case-insensitively.
  3. The offboard precheck reads the contact. They are not blocked, not DNC-flagged, not globally opted out — the precheck passes. (Had they already opted out, enrollment would stop here and the send-side suppression handles the reply’s opt-out semantics independently.)
  4. The active-enrollment guard and re-entry policy pass: no active enrollment, and the journey’s re_entry_policy: "after_cooldown_days" with a 30-day cooldown finds no terminal enrollment inside the window.
  5. The entry rate limit (entry_rate_limit_per_hour: 500) counts this hour’s enrollments — under the cap, so the enrollment dispatches.
  6. The executor starts the contact at the trigger’s first node: a condition on a contact attribute, then a sendMessage winback offer, then a waitForEvent: "purchase".
  7. Three days later the contact purchases. The CDP event router’s waiter-locator matches the purchase event to the stamped waiter; the contact resumes at the wait node and follows the edge into an exitJourney goal node: the enrollment goes terminal, the goal counter lands in journey stats, and no further steps fire.
Had the journey set entry_rate_limit_per_hour: 500 and already enrolled 500 contacts that hour, step 5 would have skipped with a log line — and the next inbound signal or router cycle would re-evaluate the contact once the window drained.

Further reading

  • Campaign lifecycle — the blast vs drip vs journey distinction this page’s fan-out feeds, and the campaign-level status machine a journey sits in.
  • Inbound message routing — the routing substrate below the inbound-reply router: how an inbound message is resolved to a contact and conversation before any journey matching happens.
  • CDP event model and CDP segment recompute — the signal producers the event and segment routers consume.
  • Frequency caps — the send-side pacing layer that stays in place underneath the journey entry rate limit.
  • Campaign holdout and lift measurement (smart-send holdout lift endpoint, covered in campaign lifecycle → terminal outcomes) — how a held-out slice turns a journey’s goal events into measured lift.