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:- 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.
- 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.
- Re-entry policy. The entry executor consults the journey’s
re_entry_policyagainst 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 oncere_entry_cooldown_dayshave 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.
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.
Node state machine
An enrollment is a row keyed to the campaign and contact, sitting inactive 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.
waitDelayparks the enrollment with aresume_attimestamp and returns — the scheduler re-enters the graph when the time passes.waitForEventparks 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.waitUntilDateparks until an absolute date, optionally read off a contact field. All three leave the enrollmentactive, so a waiting contact still counts against the active-enrollment guard above. - Branch nodes.
conditionevaluates contact fields against comparison operators and picks an edge.abSplitassigns the contact to a variant arm.channelPreferenceBranchandsmartChannelpick the send channel from the contact’s preferences or engagement signal. - Goal and exit.
exitJourneymoves the enrollment to a terminal status mid-graph — this is how “stop messaging them once they convert” is expressed.journeyEndmarks 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.
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.
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 repliesSTOP to an SMS:
- The inbound message lands and persists in the conversation.
- The inbound-reply router resolves the tenant’s running journeys with
inbound_replytriggers (cached 60s) and finds your “Winback on STOP-intent” journey, whose keyword config iskeywords: ["stop"],matchMode: "contains", channelsms. The reply body matches case-insensitively. - 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.)
- 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. - The entry rate limit (
entry_rate_limit_per_hour: 500) counts this hour’s enrollments — under the cap, so the enrollment dispatches. - The executor starts the contact at the trigger’s first node: a
conditionon a contact attribute, then asendMessagewinback offer, then awaitForEvent: "purchase". - Three days later the contact purchases. The CDP event router’s waiter-locator matches the
purchaseevent to the stamped waiter; the contact resumes at the wait node and follows the edge into anexitJourneygoal node: the enrollment goes terminal, the goal counter lands in journey stats, and no further steps fire.
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.