# Journey coverage (area 1)

Coverage is measured, not listed. A journey that exists in the editor but enters 62% of the people its trigger should catch is a gap, and so is a journey that enters people and sends them nothing, or sends to people who already reached its goal. This file holds the inventory columns, the expected journey types per business model and their default stages, the coverage ratio method and rules JC01 to JC09.

## Build the inventory

One row per:

- live journey, flow, Canvas, workflow or recurring campaign in every sender
- paused journey that still had entries in the window
- external owner of a journey type: a billing system's dunning, a store platform's order emails, an app store's billing notices, a sales or CS task that replaces an email

Fill [../assets/journey-inventory-template.csv](../assets/journey-inventory-template.csv) from read-only exports or MCP reads. Where each field lives per platform: the platform files (platform-braze.md, platform-customerio.md, platform-klaviyo.md, platform-hubspot.md, platform-iterable.md). Settings the API does not return are read in the dashboard and quoted.

Columns and allowed values (the script lower-cases everything except names, IDs, owners, goal events, trigger names and notes):

- `platform`: the sender, for example customerio, hubspot, braze, klaviyo, iterable, billing, store, app_store, crm_task.
- `journey_id`, `journey_name`: as shown in the platform. Use a stable label for external rows.
- `status`: live, paused, draft, archived or external.
- `journey_type`: a type from the model lists below, or `other`.
- `stage`: onboarding, activation, conversion, adoption, retention, expansion, winback or service. Leave it blank to use the type's default stage below; a filled value wins in the coverage-by-stage view.
- `message_class`: marketing, transactional, service or internal. Class by purpose, not by the platform setting.
- `channels`: email, sms, push, in_app, whatsapp, separated by semicolons.
- `trigger_type`: event, segment, schedule, api, form or date.
- `trigger_name`: the event, segment, list or date field that starts it.
- `trigger_source`: server, billing, store, client, sdk, sync, import, na or unknown. `client` and `sdk` mean a browser or device emits it.
- `entry_unit`: the unit that recurs: person, workspace, account, invoice, order, trial, subscription, cart, checkout.
- `recipient_role`: who receives it (workspace_admin, billing_admin, contact, shopper).
- `re_entry`: once, per_unit, cooldown_<n>d (for example cooldown_30d), always or unknown.
- `exit_on_goal`: yes, no or na. `goal_event`: the event or state that should end it.
- `message_steps`: number of messages. `max_delay_hours`: the longest wait before any message.
- `recheck_after_delay`: yes when a send-time check of current state runs before each delayed message.
- `frequency_capped`: yes, no, exempt or na. Put the exemption reason in `notes`.
- `sends_to_unsubscribed`: yes when the journey is set to reach unsubscribed users.
- `holdout`: global, journey or none.
- `owner`: the role that can pause it and owns its outcome metric.
- `last_entry_date`: YYYY-MM-DD.
- `entries_window`: entries in the window as the platform reports them. Record the metric label you used.
- `expected_window`: the source-of-truth count for the same window and unit, minus exclusions that apply at entry. Blank for scheduled sends with no trigger.
- `sends_window` (optional, for JC09): messages the journey sent in the window as the platform reports them. Record the metric label.
- `steps_not_sending` (optional, for JC09): message steps that had people reach them but sent nothing, or sit in a non-sending status, in the window.

## Expected journey types per model

These lists must match the dictionary at the top of [../scripts/inventory_audit.py](../scripts/inventory_audit.py). Required types missing with no external owner are JC01 WARN; recommended types missing are INFO.

- saas required:
  - activation_first_value (revenue-critical)
  - trial_expiry (revenue-critical; N/A without trials)
  - payment_failure (revenue-critical; external owner allowed)
  - renewal_notice (revenue-critical; N/A without annual or contract terms; external owner allowed)
  - account_security (service; external owner allowed)
- saas recommended: signup_completion, onboarding_setup, invite_collaborators, usage_limit, upgrade_or_expansion, usage_decline, cancellation, winback, product_updates.
- ecommerce required:
  - welcome_subscriber (revenue-critical)
  - cart_or_checkout_abandonment (revenue-critical)
  - order_and_shipping (service; external owner allowed)
  - winback (revenue-critical)
  - sunset_or_repermission (deliverability-critical)
- ecommerce recommended: browse_abandonment, post_purchase_cross_sell, replenishment, review_request, back_in_stock, vip_or_loyalty.
- app required:
  - onboarding_and_permission
  - activation_first_value (revenue-critical)
  - subscription_conversion (revenue-critical; N/A without subscriptions)
  - billing_issue (revenue-critical; external owner allowed for app stores)
  - lapsed_reactivation
  - account_security (service; external owner allowed)
- app recommended: habit_or_streak, winback_after_cancel, feature_announcement.
- leadgen required:
  - lead_welcome
  - nurture_by_stage (revenue-critical)
  - sales_handoff (revenue-critical; a CRM task for sales counts as the external owner)
  - customer_onboarding
- leadgen recommended: event_follow_up, recycle_closed_lost, renewal_or_expansion, newsletter.

Type sets the script uses for other rules:

- Promotional (PC07): winback, winback_after_cancel, post_purchase_cross_sell, upgrade_or_expansion, product_updates, feature_announcement, newsletter, browse_abandonment, cart_or_checkout_abandonment, back_in_stock, vip_or_loyalty, review_request, nurture_by_stage, event_follow_up, recycle_closed_lost, habit_or_streak.
- Read the copy (PC07 WARN): usage_limit. A usage or quota alert classed service or transactional is a notice about the account's standing; it becomes FAIL only when the copy carries an upgrade offer or a discount.
- Commercial facts (TD01): trial_expiry, payment_failure, renewal_notice, subscription_conversion, billing_issue, order_and_shipping, cancellation.
- Account states (TD01): activation_first_value, signup_completion, onboarding_setup, invite_collaborators, usage_limit, usage_decline, onboarding_and_permission, lapsed_reactivation, customer_onboarding.

Default stage per type (used when the row's `stage` is blank, and to place missing types in the coverage-by-stage view):

- onboarding: signup_completion, onboarding_setup, welcome_subscriber, onboarding_and_permission, lead_welcome, customer_onboarding
- activation: activation_first_value
- conversion: trial_expiry, subscription_conversion, cart_or_checkout_abandonment, browse_abandonment, back_in_stock, nurture_by_stage, sales_handoff, event_follow_up
- adoption: invite_collaborators, product_updates, feature_announcement, habit_or_streak, review_request, newsletter
- retention: payment_failure, billing_issue, renewal_notice, usage_decline, cancellation, replenishment, vip_or_loyalty
- expansion: upgrade_or_expansion, post_purchase_cross_sell, renewal_or_expansion
- winback: winback, winback_after_cancel, lapsed_reactivation, recycle_closed_lost, sunset_or_repermission
- service: account_security, order_and_shipping, usage_limit

Mark a required type N/A only with a reason the user confirms (no trials, no annual terms, no subscriptions) and pass it with `--not-applicable`.

## Coverage ratio

`ratio = entries_window / expected_window`, same window, same unit.

- Expected comes from the system that commits the fact: warehouse, billing system, store. Never from the messaging platform's own segment count.
- Subtract only exclusions that apply at entry and are written down: holdout, region, internal and test accounts, a documented eligibility filter. Do not subtract unsubscribed people when the platform counts them as entries and suppresses at send.
- Compare like with like. A journey that enters per workspace is compared with workspaces, not users. Record the platform's metric label as shown.
- Platform facts that change the expected count (verify live before stating):
  - Klaviyo: trigger filters and profile filters are checked at entry, so a profile filter keeps people out of the entry count.
  - Braze: the dashboard entry audience also applies to API-triggered Canvases.
  - Customer.io: its journeys page says event, object, relationship, date and form triggers can retry an initially unmet filter for up to 30 minutes, so a late attribute can still admit someone (verify live).
  - HubSpot and Iterable: read how enrolment filters and re-enrolment count entries before comparing.

Source-of-truth query pattern (rename tables and columns; keys are internal IDs only; interval syntax differs by warehouse):

```sql
-- Expected entries for one trigger in the window, minus exclusions that apply at entry.
select count(distinct w.workspace_id) as expected_window
from analytics.workspaces w
left join analytics.data_source_connections c
  on c.workspace_id = w.workspace_id
 and c.connected_at < w.created_at + interval '24 hours'
where w.created_at >= '2026-09-15'
  and w.created_at <  '2026-10-15'
  and c.workspace_id is null          -- still unconnected when the first message is due
  and w.is_internal = false           -- internal and test accounts (SG04)
  and w.holdout_group <> 'control'    -- the global holdout applies at entry
```

Bands, rated only when expected is 100 or more:

- 0.90 to 1.10: PASS.
- 0.75 to 0.90, or 1.10 to 1.25: WARN. Trace 5 missing or extra occurrences end to end.
- Below 0.75: FAIL, trigger leakage. Split entries by source (web, API, mobile, import).
- Above 1.25: FAIL, duplicate or unintended entries. Check producers, retries and imports (TD02 to TD04).
- Expected below 100: report the counts and write "not rated (low volume)".

## Rules

### JC01 expected-journeys

- PASS: every required type has a live journey or an external owner with evidence it runs (one sample message or setting).
- WARN: a required type has neither. Priority P1 when revenue-critical or deliverability-critical, P2 otherwise.
- INFO: a recommended type is missing.
- Detect: inventory_audit.py against the model list.
- Confirm: seed S01 for a journey that should exist; for an external owner, one saved message.

### JC02 coverage-ratio

- Bands above. Detect: entries over expected per live row. Evidence: the source query text, the platform count with its label.
- Confirm: trace 5 missing or extra occurrences from the producer to the entry log.

### JC03 stale-live-journey

- FAIL: live, 0 entries in 30 days, and the source shows qualifying occurrences. The usual cause is a renamed or retired trigger event, or an entry filter on a field that no longer updates.
- WARN: live, 0 entries, no occurrences in the source: dead or seasonal. Ask the owner.
- No source count yet (expected_window blank): WARN until the count arrives, then FAIL or WARN by the two rules above. Never write that the source shows nothing when nobody counted it.
- Evidence: last entry date, source count, the event name history from the tracking plan.

### JC04 exit-on-goal

- FAIL: a journey with 2 or more messages or any delay has no exit or goal check for the outcome it promotes.
- PASS in the script is configuration-only. Read the exit settings in the platform and look for sends after the goal event in the send log.
- Confirm: seed S02 (goal reached during a delay).

### JC05 recheck-after-wait

- WARN: a delay of more than 1 hour before a message with no send-time recheck of current state.
- FAIL: the same, and the copy states the unfinished state ("you have not connected a source yet") or offers a discount.
- Detect: inventory_audit.py, then read the message content. Confirm: seed S02.

### JC06 re-entry-matches-unit

- WARN: re-entry blank or unknown, or contradicting the unit (once for a per-object unit means a second invoice or workspace for the same person cannot enter; always for a person-level welcome means repeats).
- FAIL: overlapping active instances for the same unit observed in the entry history.
- Detect: inventory_audit.py; entry history for a sample of 20 IDs.

### JC07 collisions

- WARN: two or more live or external journeys of the same type, or two live marketing journeys on the same trigger, across any senders, with no mutual exclusion or priority rule.
- FAIL: message_pressure.py journey pairs show both reaching the same people within 24 hours.
- Typical pairs: lifecycle payment reminder plus billing-system dunning; trial expiry in both the lifecycle tool and the CRM.

### JC08 ownership

- WARN: no named owner, or no written outcome metric with a window.

### JC09 live-journey-not-sending

- FAIL: a live journey had entries in the window, and it sent 0 messages, or a message step that people reached sent nothing or sits in a non-sending status. Entries look healthy, so JC02 passes.
- WARN: 0 sends, but the longest delay is at least as long as the 30-day window, so nobody may have reached a message yet. Check entrants older than the delay; FAIL if they received nothing.
- Detect per platform (status labels: verify live before quoting):
  - Klaviyo: `get_flow_report` for sends per flow message, and each flow message's status (a message left in a manual or draft state does not send).
  - Braze: `get_canvas_data_series` for sends per step; on an active Canvas, edits saved as a draft and never launched do not send.
  - Customer.io: step metrics and each message's sending behaviour. Outside a holdout test, Queue Draft holds messages as drafts and Don't Send sends nothing (labels from the Customer.io holdout page; behaviour: verify live).
  - HubSpot: workflow action history for the email action (errors and skips) and the automated email's sent count for the window.
  - Iterable: journey and campaign send counts per tile (verify live).
- Evidence: entries and sends per step for the window with the metric labels, and the step status quoted.
- Confirm: seed S01 through the real producer reaches the step and receives the message.

## Collisions and priority

The audit expects one priority order across senders. If decisions.md has none, compare against this default and record the gap:

1. Incident and service messages.
2. Billing recovery.
3. Onboarding tasks the customer still has to finish.
4. Adoption.
5. Promotion.

What the losing journey does must be written down: suppress, defer under a named rule, exit obsolete instructions (setup tips after activation), and record the suppression reason. A priority order is an operating rule, not a legal classification: it never makes a promotion transactional. Evidence comes from message_pressure.py journey pairs and FC09 block reasons.

## Journey contract fields every fix restates

A change request that touches a journey restates its contract, so the fix can be tested:

- entity and recipient role (a workspace event, sent to the billing admin)
- entry event with its occurrence ID
- action timing with the send-time recheck
- exit per stop reason
- re-entry per unit
- finite step budget and expiry
- outcome metric with its window
- owner who can pause it

Designing or rewriting the journey itself belongs to journey-spec-and-qa (planned). Until it ships, use the fields above as the specification.

## Reading inventory_audit.py output

- One line per finding: level, rule, name, journey and detail. FAIL lines come first.
- The coverage map lists each required and recommended type with the covering rows, MISSING, or N/A.
- Coverage by stage lists, per lifecycle stage, the live and external journeys, then the model's missing required and recommended types. Use it for the report's stage view.
- Coverage ratios list every live row: rated, not rated (low volume), not rated (no count), or 0 entries. A last line lists live rows that JC09 could not rate because `sends_window` and `steps_not_sending` are blank.
- A JC03 line on a live journey with 0 entries says which case applies: the source shows occurrences (FAIL), the source shows none (WARN, dead or seasonal), or `expected_window` is blank (WARN: get the source count; FAIL if it shows qualifying occurrences, else WARN).
- Counts may carry thousands separators, as dashboard exports write them (1,240). A decimal comma (1,5), `nan` and `inf` are rejected as not a number.
- The summary line ends with "Nothing was changed." Exit status 1 means at least one FAIL; 2 means bad input, with the rows and columns to fix, a file that is not a UTF-8 CSV (save Excel files as CSV UTF-8), or a `--not-applicable` type that is not in the chosen model.
- The "Not checked by this script" line lists the rules the agent must still apply by hand or with the other scripts.
