> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Journey enrollment fan-out: three routers, one entry chain

> How contacts enter a running journey — the three enrollment routers (inbound-reply keyword, segment entry/exit, CDP-track events), the shared offboard precheck and re-entry guard they all funnel through, the per-journey entry rate limit, wait-node semantics, and the failure posture that leaves an unmatched contact untouched.

# Journey enrollment fan-out

The [campaign lifecycle](/concepts/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:

| Source                    | Match semantics                                                                                                                                                                                      | Re-entry behavior                                                                                                                                  |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Inbound-reply keyword** | A contact replies; the reply body is matched against the journey's configured keywords (exact / starts-with / contains, case-insensitive by default, multi-keyword OR, optional channel restriction) | Same shared guard — an active enrollment blocks a second entry; a prior terminal enrollment is admitted or denied by the journey's re-entry policy |
| **Segment entry / exit**  | The segment-refresh tick computes the membership delta: contacts that **joined** the tracked segment drive the segment-entry router, contacts that **left** drive the segment-exit router            | Same shared guard                                                                                                                                  |
| **CDP-track event**       | A `track` event whose name (case-insensitive) and properties match the trigger's event filter                                                                                                        | Same shared guard                                                                                                                                  |

One more path deliberately bypasses this page's model: the [single-contact enrollment endpoint](/concepts/campaign-lifecycle) 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:

| Router                       | Signal it consumes                                       | Match predicate                                                                                                                |
| ---------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Inbound-reply keyword router | Each inbound message, after it lands in the conversation | `triggerType: "inbound_reply"` + keyword match (exact / starts\_with / contains, OR across keywords) + optional channel filter |
| Segment-entry router         | Segment membership delta on the refresh tick             | `triggerType: "segment"` + `config.segmentId` equal to the segment that gained members                                         |
| Segment-exit router          | Segment membership delta on the refresh tick             | `triggerType: "segment_exit"` + `config.segmentId` equal to the segment that lost members                                      |
| CDP-track event router       | Each CDP `track` event                                   | Event name match (case-insensitive) plus an optional event-properties filter                                                   |

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](/concepts/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](/concepts/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](/concepts/cdp-event-model) and [CDP segment recompute](/concepts/cdp-segment-recompute-model) — the signal producers the event and segment routers consume.
* [Frequency caps](/concepts/frequency-caps-model) — 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](/concepts/campaign-lifecycle)) — how a held-out slice turns a journey's goal events into measured lift.
