Journey stage machine (T1.4)
Canonical state diagram + transition rules for CX Orbit. This is a documentation/spec lock. It does not add a new engine.
Runtime already follows this machine:
| Layer | Where |
|---|---|
client.current_stage |
@cx-orbit/profile applyStageTransition (T2.9) |
| Funnel first-reach | @cx-orbit/web applyStageTransitionForHistory (T3.2) — same rules |
| Stage enum | JOURNEY_STAGES in packages/profile/src/constants.ts |
Related: METRICS-AND-RISK.md §4, RISK-RULES-V1.md §5 and §9, DATA-MODEL.md §3–4, event dictionary SEC-02 in simiaroom-cx-v5.md.
1. SEC-02 alignment
| SEC-02 principle | How this machine encodes it |
|---|---|
| Path is not linear | Pause, completion, churn, return, and rematch are first-class. Funnel spine is a display order, not the only allowed life. |
| State from timed, traceable events; a click is not a transition | Only canonical backend/system events in §4 move current_stage. Behavioral clicks (page_viewed, therapist_options_viewed, login_clicked, assessment_started, …) are stage-neutral. |
| Pause, cadence drop, completion, and churn are independent | Separate stages and separate flags. T3.3 / T3.4 must not collapse them into one “stopped” rule. |
| Interpretation uses history, not only current state | Funnel counts first-reach. Risk/retention use flags, session_count, cadence, rematch count. |
| Identity by ID-01 rules, not IP | identified requires authentication_succeeded or identity_link_created. Anonymous-only events never enter the funnel cohort. |
| Acute crisis leaves the normal path | Not a journey stage. Crisis routing is out of this machine (no crisis value in JOURNEY_STAGES). |
2. Stages listed for funnel display
Two different uses of the same enum. Do not mix them.
2.1 Linear spine (T3.2 conversion table)
Displayed in this order. Conversion is reached(N) / reached(N−1). These are not current-stage buckets.
| Order | Stage id | Authoritative first-reach event | Funnel label intent |
|---|---|---|---|
| 1 | identified |
authentication_succeeded or identity_link_created |
Resolved client entered |
| 2 | assessed |
assessment_completed |
Assessment finished |
| 3 | matched |
therapist_selected |
Therapist chosen (initial or later) |
| 4 | booked |
booking_confirmed |
Booking confirmed |
| 5 | paid |
payment_succeeded |
Payment succeeded |
| 6 | in_treatment |
session_completed |
At least one completed session |
identified has no previous stage; conversion is null (UI —).
Cohort size = clients whose first identified event falls in the required [from, to] window.
2.2 Non-linear branches (T3.2 side counts, not denominators)
| Branch id | How counted | current_stage value? |
|---|---|---|
paused |
First-reached paused via therapy_pause_declared |
Yes — paused |
returned_from_pause |
Pause then therapy_resumed |
No — resume writes in_treatment |
completed |
First-reached completed via treatment_completed |
Yes — completed |
churned |
First-reached churned via stop / churn confirm |
Yes — churned |
A client who paused after session 1 still keeps in_treatment first-reach. Branches are extra, not a replacement column.
There is no funnel branch returned_from_churn. Churn → return is a current_stage path only (§7). T3.2 must not grow a seventh conversion stage for it.
2.3 Serving-only values (not funnel columns)
| Stage | Meaning |
|---|---|
unknown |
Initial current_stage before any identifying event. Not a funnel row. Incomplete identity is excluded from T3.2 / T3.3 / T3.4 denominators. |
JOURNEY_STAGES = unknown, the six spine ids, paused, completed, churned.
3. State diagram
Linear arrows are the happy-path spine. Dashed / labeled arrows are non-linear. Skip-forward on the spine is allowed by the engine when a later authoritative event arrives without the earlier one (illegal for producers — §8).
stateDiagram-v2
[*] --> unknown
unknown --> identified: authentication_succeeded / identity_link_created
identified --> assessed: assessment_completed
assessed --> matched: therapist_selected
matched --> booked: booking_confirmed
booked --> paid: payment_succeeded
paid --> in_treatment: session_completed
in_treatment --> paused: therapy_pause_declared
paused --> in_treatment: therapy_resumed
in_treatment --> completed: treatment_completed
paused --> completed: treatment_completed
in_treatment --> churned: therapy_stop_declared / churn_confirmed / churn_reason_submitted
paused --> churned: therapy_stop_declared / churn_confirmed / churn_reason_submitted
churned --> in_treatment: therapy_resumed
Rematch does not appear as a stage: therapist_selected with selection_context=replacement stays on the current stage (§7.2).
Pause expiry does not appear as a stage: current_stage stays paused until resume, stop, or completion. Risk/retention flip via pause_expired, not via a new stage id.
4. Transitions driven by events (not clicks)
Replay events in UTC occurred_at order; tie-break source_event_id lexicographic (same as T2.9 / T3.2).
4.1 Stage-changing events
| Canonical type | From | To | Notes |
|---|---|---|---|
authentication_succeeded |
unknown or any non-terminal stage earlier than identified |
identified (or unchanged if already later) |
Also the usual cohort-entry event |
identity_link_created |
same | identified |
ID-01 merge; still not IP-only |
assessment_completed |
non-terminal, earlier than assessed |
assessed |
Clicks on questions do not count |
therapist_selected |
non-terminal, earlier than matched |
matched |
Replacement: see §7.2 (usually no stage change) |
booking_confirmed |
non-terminal, earlier than booked |
booked |
|
payment_succeeded |
non-terminal, earlier than paid |
paid |
|
session_completed |
non-terminal, earlier than in_treatment |
in_treatment |
If current is paused, stay paused (session still increments session_count) |
therapy_pause_declared |
any except that linear advance is skipped | paused |
Sets current_pause_flag |
therapy_resumed |
paused, churned, or completed |
in_treatment |
Else no-op. Clears pause flag. Does not clear completion_flag |
treatment_completed |
any | completed |
Sets completion_flag; clears pause flag |
therapy_stop_declared |
any | churned |
Clears completion_flag |
churn_confirmed |
any | churned |
Same. risk_v1 confirms churn from SIG-STOP, not from gap |
churn_reason_submitted |
any | churned |
Stage follows stop; reason is payload |
Terminal for linear advance: completed and churned. Later spine events (session_completed, payment_succeeded, …) do not move those stages. Return from churn/completion uses therapy_resumed only.
Monotonic spine: a later spine event may skip forward (identified + payment_succeeded → paid). An earlier spine event never moves the client backward.
4.2 Stage-neutral events (recompute may run; stage must not jump)
These may trigger SYS-01 profile recompute. They must not change current_stage:
| Canonical type | Why it is not a stage |
|---|---|
payment_failed |
Failure is a risk signal (SIG-PAY-FAIL), not “un-paid” |
booking_cancelled |
Cancel does not regress booked (T2.9 residual, locked here) |
session_no_show_recorded |
Does not increment session_count; does not enter in_treatment |
unplanned_gap_detected |
CON-S01 indicator; not churn |
risk_state_entered |
Risk overlay; risk does not drive the stage machine |
churn_suspected |
Suspicion ≠ churned |
internal_case_resolved |
Ops action outcome |
Clicks and behavioral events (not in the table, default = no stage change): page_viewed, landing_page_viewed, login_clicked, assessment_started, assessment_question_answered, therapist_options_viewed, therapist_profile_viewed, booking_started, return_intent_detected, and any other GA/product click. SEC-02: a click is not a transition.
therapist_change_requested / therapist_change_reason_submitted are rematch intent, not a stage. Stage stays put until a later therapist_selected (usually still no stage change — §7.2).
5. Pause and completion suppressions
Stage, funnel, retention, and risk all see the same events. They apply different suppressions. Do not reuse continuation_status=stopped as churn (T2.4 maps completion that way; T3.3 must use completion_flag).
5.1 Declared pause — `therapy_pause_declared` (CON-04)
| Layer | Rule |
|---|---|
| Stage | current_stage = paused. current_pause_flag = true. |
| Funnel (T3.2) | Count branch paused. Do not remove linear first-reach. Resume sets branch returned_from_pause and current_stage = in_treatment. |
| Retention (T3.3) | Never churn while pause is active. CON-S01 cadence breach suppressed. |
| Risk (T1.7 / T3.4) | Suppress SIG-GAP-CADENCE, SIG-GAP-LONG, SIG-GAP-FALLBACK, SIG-NO-BOOKING. Cap level at needs_attention. High-risk tab excludes planned pauses. Do not emit CHR-S02 during active pause. |
Active pause = current_pause_flag AND NOT pause_expired.
pause_expired (does not change stage):
| Condition | Result |
|---|---|
expected_return_date present and as_of date > that date, no later therapy_resumed |
Expired (T2.4 pause_return_breach) |
expected_return_date present and as_of date ≤ that date |
Active |
expected_return_date missing |
Active for 14 UTC calendar days after the pause event, then expired |
When expired: T3.3 counts a dangerous-pause indicator (not churn). T3.4 lifts the cap and fires SIG-GAP-PAUSE-BREACH. current_stage remains paused until therapy_resumed, stop, or completion.
5.2 Completion — `treatment_completed` (CHR-S04)
| Layer | Rule |
|---|---|
| Stage | current_stage = completed. completion_flag = true. Pause flag cleared. |
| Funnel (T3.2) | Count branch completed. Keep linear first-reach. |
| Retention (T3.3) | Not churn. Use completion_flag, not session continuation_status. |
| Risk (T1.7 / T3.4) | Stop scoring. Return healthy, risk_reasons = []. Exclude from at-risk and churn-as-failure. Do not emit CHR-S01/S02/S03. |
Completion is a successful operational outcome. It is not a clinical claim.
5.3 True churn (for contrast)
therapy_stop_declared or churn_confirmed → current_stage = churned, completion_flag = false.
T3.3 churn-after-N requires a stop event, completion_flag = false, pause not active, and session_count = N. Gap and expired pause are not churn.
T3.4: SIG-STOP → high_risk, queue kind churned, not preventable at-risk.
6. Flags vs stage (do not collapse)
| Flag | Set by | Cleared by | Independent of |
|---|---|---|---|
current_pause_flag |
therapy_pause_declared |
therapy_resumed or treatment_completed |
Cadence, churn |
completion_flag |
treatment_completed |
therapy_stop_declared or churn_confirmed |
Pause |
pause_expired |
Clock vs return date / 14-day fallback | Resume (flag off) | Stage id stays paused until resume/stop/complete |
therapist_change_count |
Replacement therapist_selected |
Never reset by rematch | Stage, session_number |
7. Return and rematch paths
7.1 Return
| Prior state | Return event | Next current_stage |
Funnel |
|---|---|---|---|
paused (active or expired) |
therapy_resumed |
in_treatment |
Branch returned_from_pause |
churned |
therapy_resumed |
in_treatment |
First-reach churned kept; no extra T3.2 branch. T3.4 SIG-STOP lifts. T3.3 still treats a stop-at-or-before-as_of as churn (no resume override in the retention fact builder) — see §8 |
completed |
therapy_resumed |
in_treatment |
Illegal producer sequence (§8). Stage follows resume; completion_flag stays true so T3.3/T3.4 still treat as completed |
Not return: authentication_succeeded, login_clicked, page_viewed, return_intent_detected, or a new booking/session while still churned/completed (linear advance is blocked). SEC-02: login after churn is not reactivation.
Same-therapist vs rematch-on-return is a property (selection_context=resume / replacement), not a new stage.
7.2 Rematch
Rematch is a therapist-change overlay on the existing continuum. It is not a funnel stage and must not reset session_number (T2.4).
| Event | Stage effect | Other effect |
|---|---|---|
therapist_change_requested (CHG-01) |
None | Intent only |
therapist_change_reason_submitted (CHG-02) |
None | Reason payload |
therapist_selected with selection_context=replacement |
Stay on current stage if already matched or later |
Increment therapist_change_count when therapist_id changes; T2.4 sets therapist_changed_since_previous on the next completed session |
Later booking_confirmed / session_completed |
Normal spine rules | Continuum continues N, N+1, … |
T3.4 SIG-THERAPIST-CHG may fire (attention, not high by itself). Completion still suppresses it.
8. Illegal and ambiguous transitions
Producers must not emit these. The engine still has a deterministic last-event / monotonic rule so replay does not crash. Funnel conversion may look wrong if producers skip the spine.
| Case | Why it is illegal / ambiguous | Engine behavior (locked) | Analytics |
|---|---|---|---|
| Click / behavioral event as a stage move | SEC-02 | No-op | Not in T3.2 spine |
assessment_started without assessment_completed |
Incomplete assessment | Stay pre-assessed |
Not funnel.reached.assessed |
Skip-forward (e.g. payment_succeeded with no booking_confirmed) |
Missing authoritative spine event | Jump to the later stage | First-reach may skip a column; conversion can exceed 100% — treat as DQ, not as a new stage |
Backward spine event (e.g. assessment_completed after in_treatment) |
Out-of-order duplicate | No-op; stay later | First-reach already recorded |
session_no_show_recorded or booking_cancelled as drop to a previous stage |
Cancel/no-show ≠ un-book / un-treat | Stage-neutral | T3.3 does not number those rows |
churn_suspected or unplanned_gap_detected → churned |
Gap ≠ churn; suspicion ≠ confirm | Stage-neutral | T3.3 indicator / T3.4 signal only |
| Pause declared before any session | Pause is a treatment-lifecycle branch | Stage becomes paused |
Branch paused may appear without in_treatment; still not a conversion denominator |
session_completed while paused |
Session during planned pause | Stay paused; session_count still +1 |
First-reach in_treatment unchanged if already set |
Login / auth after churned or completed |
Click/login ≠ return | Stay terminal | No returned_from_pause |
therapy_resumed after a stop (churn → return) |
Stage returns; T3.3 churn metric does not | Stage → in_treatment; T3.4 SIG-STOP requires no later therapy_resumed / session_completed |
T3.2 keeps first-reach churned. T3.3 isChurned stays true if any stop exists at or before as_of (residual: retention snapshot ≠ current stage) |
therapy_resumed while not paused/churned/completed |
Resume without a leave | No-op | |
therapy_resumed after treatment_completed |
Completion and return conflict | Stage → in_treatment; completion_flag stays true |
Risk/retention still completed |
Both treatment_completed and a stop event |
Mutually exclusive outcomes | Last timestamp wins for stage and flags (stop clears completion) | Do not count as both completed and churned in T3.3 (flag after last event) |
IP-only or shared-device merge into identified |
Violates ID-01 | Identity layer refuses merge (T2.3) | No extra funnel client |
| Crisis / out-of-scope flag as a stage | SEC-02 crisis is a redirect | No crisis stage |
Out of this machine |
9. Product journey refs J-01…J-12 (not extra funnel columns)
T1.1 maps product reality onto J-01…J-12: JOURNEY-GAP-ANALYSIS.md. This machine collapses them for P0 display:
| J-ref | Product idea | Machine |
|---|---|---|
| J-01 Entry | Anonymous / explore / login click | Not a stage. identified only after auth or identity link |
| J-02 Assessment | Start / progress / complete | assessed on assessment_completed only |
| J-03 Therapist explore / select | Views vs select | matched on therapist_selected |
| J-04 Booking | Start vs confirm | booked on booking_confirmed |
| J-05 Payment | Fail vs success | paid on payment_succeeded; fail is stage-neutral |
| J-06 Session | Scheduled / no-show / completed | in_treatment on session_completed |
| J-07 Feedback | ORS/SRS | Stage-neutral; risk/feedback metrics |
| J-08 Continue / pause | Cadence vs declared pause | paused + flags; cadence is not a stage |
| J-09 Therapist change | Rematch subflow | Overlay; stay on current stage |
| J-10 Churn / completion | Independent outcomes | churned vs completed |
| J-11 Return | Resume after pause or churn | therapy_resumed → in_treatment |
| J-12 Internal action | Cases | Stage-neutral |
10. Test Agent walkthroughs
Walk each scenario event by event against §3–§4. Expected current_stage is after that event. First-reach is the set of stages entered at least once (T3.2).
W1 and W2 reuse T3.2 fixture clients in fixtures/journey-funnel/p0-cohort.json (cohort window 2026-01-01 … 2026-06-30). W3 extends usr_churn with an explicit resume (not in that fixture).
W1 — Happy path (`usr_full_a`)
| # | Event | occurred_at |
current_stage after |
|---|---|---|---|
| 0 | (none) | — | unknown |
| 1 | authentication_succeeded |
2026-01-05T08:00Z | identified |
| 2 | assessment_completed |
2026-01-06T09:00Z | assessed |
| 3 | therapist_selected (selection_context=initial, thr_a) |
2026-01-07T10:00Z | matched |
| 4 | booking_confirmed |
2026-01-08T11:00Z | booked |
| 5 | payment_succeeded |
2026-01-08T11:05Z | paid |
| 6 | session_completed |
2026-01-20T10:00Z | in_treatment |
First-reach: identified, assessed, matched, booked, paid, in_treatment.
Branches: none (not paused, not returned, not completed, not churned).
Suppressions: none. If session_count ≥ 1, T3.4 may score; T3.3 includes the client in reached(1).
Not in this path: page_viewed / therapist browse would not have moved the stage if inserted between 2 and 3.
W2 — Pause then return (`usr_pause_ret`)
Same spine as W1 through session_completed (2026-03-01) → in_treatment, then:
| # | Event | occurred_at |
current_stage after |
Flags |
|---|---|---|---|---|
| 7 | therapy_pause_declared (current_pause_flag=true) |
2026-03-15T10:00Z | paused |
pause on; completion off |
| 8 | therapy_resumed |
2026-04-01T10:00Z | in_treatment |
pause off |
First-reach: all six linear stages plus paused.
Branches: paused = yes, returned_from_pause = yes, completed = no, churned = no.
T3.2: client still counts in in_treatment (first-reach kept).
T3.3: while paused (and not expired), not churn. Gap signals suppressed.
T3.4: while active pause, gap signals suppressed; level capped; not on the high-risk tab. After resume, gap scoring resumes against cadence.
Expired-pause variant (not this fixture): if as_of passed expected_return_date with no resume, stage stays paused, T3.3 dangerous-pause indicator, T3.4 SIG-GAP-PAUSE-BREACH. Still not churn.
W3 — Churn then return (synthetic on `usr_churn`)
Use usr_churn through churn_confirmed, then add resume. Login in between must not return.
| # | Event | occurred_at |
current_stage after |
Notes |
|---|---|---|---|---|
| 1–6 | Same spine as fixture usr_churn through session_completed 2026-03-05 |
— | in_treatment |
First-reach full spine |
| 7 | churn_confirmed |
2026-03-20T10:00Z | churned |
completion_flag false. T3.3 churn-after-1. T3.4 SIG-STOP, not preventable |
| 7b | authentication_succeeded (hypothetical login) |
any later | churned |
Illegal to treat as return. Linear advance blocked |
| 8 | therapy_resumed |
2026-04-10T10:00Z | in_treatment |
Only legal return |
First-reach: linear spine + churned. Resume does not add a T3.2 returned_from_pause branch (no pause event).
T3.3 (step 7 snapshot, as_of after confirm, before resume): churned with session_count = 1. Pause/completion do not apply.
T3.3 (step 8 snapshot): a stop event still exists at or before as_of, so the retention churn flag stays true (T3.3 has no resume override). That is intentional residual, not a stage-machine failure. Test Agent should still accept step 8 as a stage return.
T3.4: after step 7, SIG-STOP → churned queue, not preventable. After step 8, SIG-STOP does not fire (therapy_resumed is later than the stop); score as in-treatment.
11. Out of scope (explicit)
- T1.1 product-vs-J-ref gap interviews (analysis doc:
JOURNEY-GAP-ANALYSIS.md; named ops/PM interview still residual) - New funnel columns, new
JOURNEY_STAGESvalues, or engine refactors - Crisis routing as a stage
- P1 behavioral funnel (assessment start, therapist view) as conversion stages — T3.2 residual stands
- Live Postgres serving (fixture-first, same as T3.2)
12. Change log
| Date | Change |
|---|---|
| 2026-08-12 | T1.4 initial lock: spine + branches aligned with T3.2 / T2.9; pause/completion suppressions aligned with T3.3 and risk_v1; return/rematch and illegal transitions documented; three Test Agent walks. |