Littledata MonitorAI guide

Customer health score — what makes a store Red, Yellow or Green

The customer health score is a single number that tells the Customer Success team how each paying store is doing. It rolls up engagement signals (is there a champion on the account, are they logging in, what data is flowing) and risk signals (a connection was removed, a competitor product is installed, a support issue is open) into one RAG stage — 🔴 Red, 🟡 Yellow, or 🟢 Green — that shows on the store row in cip.littledata.io and on the company sidebar in Intercom.

It is not a forecast and it is not a churn probability. It is a CSM-oriented checklist: every factor that moves the score is something a human can act on.

Who gets scored

Only customers that are all of the following:

Stores outside this set don’t appear in the RAG list and keep their previous score, if any, until they re-qualify. The one exception is the Recent positive call toggle (below) — flipping that for any customer recomputes a score on the spot, even if the customer has no destination connected yet.

History

Every time the nightly run (or a “Recent positive call” toggle) changes a store’s score or its “why” lines, a point is appended to customerHealthScoreHistory — score, stage, lines, lifecycle phase. GET /customers/{shopName}/health-history serves it to the CIP dashboard’s health chart. Unchanged runs write nothing, so the timeline is a list of events. The first time a store churns, syncCustomersCron also freezes its score, MRR and any already-disconnected destinations into customers.churnSnapshot, so a Green churn stays visible as a Green churn.

How to read the result

Stage Score Meaning
🔴 Red ≤ 0 At risk — needs attention now.
🟡 Yellow 1 or 2 Worth watching; one nudge in the wrong direction and they slide to Red.
🟢 Green ≥ 3 Healthy. No intervention needed.

Thresholds were calibrated against the actual score distribution to land roughly 45% Red / 19% Amber / 36% Green — the closest integer-cutoff split to a 1/4 – 1/4 – 1/2 target given that ~33% of stores cluster at score 0.

The exact score is shown alongside the stage so two Greens with different headroom can be told apart (e.g. +8 is far more robust than +1).

Every store also carries a “why this score” list — the same factors below, with their +N / −N contributions — so a CSM can see at a glance which lever moved them.

What pushes the score up

Factor Points What it means
Champion linked +3 The Intercom company has been marked as having an internal champion (someone inside the store who advocates for Littledata). Set via the Champion? attribute on the Intercom company (not on a contact tag — that just identifies which person to use for login timing).
Recent positive call +2 The CSM has flipped the “Recent positive call” toggle in cip.littledata.io to mark that they’ve had a good conversation with the store recently. Entirely manual — no automation guesses this.
Two or more connections +1 The store has at least two live destination connections — GA4 counts as one of them. One connection earns nothing, and a third doesn’t earn a further point on this axis.
Two or more non-GA4 connections +1 Stacks on the point above, so two real destinations are worth +2 and GA4-plus-one is worth +1. This is the sharpest retention signal we have measured: across the paying base plus everything churned since Sept 2025 (n=2,014), churn falls from 25.8% at one non-GA4 destination to 15.2% at two, and flattens after that. GA4 still earns its place in the count above — at one non-GA4 destination, stores with GA4 churn 21.6% against 29.5% without — but “GA4 plus one” retains materially worse than two real destinations, so the two states must not score the same.
Google Ads on the Littledata action +1 Google Ads is connected and a Littledata conversion action is the account’s primary conversion goal, so Smart Bidding is actually bidding on Littledata’s data. Read from the googleAdsGoalConfig audit check at Success or Info (Info means Littledata is primary alongside a GA4 purchase action — still primary). A connected account whose Littledata action sits as secondary earns nothing.
Littledata Klaviyo flows live +1 Klaviyo is connected and the klaviyoActiveLittledataFlows check is at Success — the Littledata-triggered flows are switched on and sending. Connecting Klaviyo without setting the flows live is worth nothing, because nothing is being sent.
A destination returning over 10× +1 At least one destination earns more than 10× the monthly plan cost on its own, from the same per-destination returns the value hero cards show (roiByDestination). Never a cross-destination sum — two mediocre channels can’t add up to a point neither earned.
Bot Protection or Profit Signal +1 The merchant has switched on Bot Protection (Connection.settings.filterUnidentifiedEvents) or Profit Signal (Connection.settings.profitOptimisation) for at least one destination. One point however many connections carry them — the question is whether the merchant has found the features at all.
Event Editor in use +1 The store has at least one custom event or customised standard event in the Event Editor, from ld-bi.ShopFeatureAdoption_fact mirrored nightly onto Customer.eventEditor by syncCustomersCron. Drafts alone earn nothing — nothing they define reaches a destination. 51 stores on 2026-10-07.
Shopify Markets through Littledata +1 More than two active Shopify Markets and Littledata’s Markets source switched on, from the shopifyMarketsActive check’s own activeMarketCount / littledataMarketsActivated. Five markets with no Littledata Markets source is a job we aren’t yet doing for them, so it earns nothing.

The eight adoption signals are worth a point each, deliberately. Connecting a pixel is one step; the rest are what turn a connection into value. That is why the old “+3 for a second non-GA4 destination” is now worth two points rather than four — a GA4 store that bids Google Ads on the Littledata action and runs live Klaviyo flows is a healthier account than one with four pixels wired up and nothing switched on.

Connections come from the live inventory, not the audited destinations. The count is the union of connectedDestinationTypes (every live ld-shopify Connection) and the audited destinations[] — the same keys the connection-removal diff runs on, so “two connections” means the two a CSM sees on the customer page. destinations[] holds just what MonitorAI audits, so Segment was invisible to it: 132 paying Segment customers had no audited destination at all and went unscored.

Bot Protection and Profit Signal are synced, not audited. They live in each Connection’s settings in ld-shopify-prod, so syncCustomersFromShops copies the connection names carrying them onto Customer.botProtectionConnections / Customer.profitSignalConnections on every run. Fleet-wide that is ~222 stores with Bot Protection on and ~27 with Profit Signal.

What pulls the score down

Factor Points What it means
No Champion login in 30+ days −1 The most recent login from anyone tagged Champion on the store’s Intercom contacts was 30–89 days ago. (Non-champion logins are intentionally ignored — the question is whether the advocate is still active.)
No Champion login in 90+ days −2 Same signal, 90+ days ago. Replaces the 30-day penalty (not added on top). If the store has no Champion contact at all, this signal doesn’t fire.
Champion left company −4 A contact tagged Champion now shows a different current employer on LinkedIn. The advocate is gone and the +3 “Champion linked” is describing someone who no longer works there, so this is the largest penalty alongside the app embed. Detected by checkContactEmployersCron through marketing-automations’ employer-check API (Bright Data LinkedIn snapshots); the “why this score” line names the person. Clears when CS moves the Champion tag to a new contact (or the next check sees them back at the company).
Champion changed role −1 Same check, same employer, but the champion’s title has changed since we started tracking them (a Head of Ecommerce who is now VP Brand may no longer own the Littledata relationship). Not applied on top of “left company”. Best-effort: since LinkedIn’s Nov 2025 policy change titles mostly come from the headline.
Connection removed −2 A destination integration was connected on the last refresh and is no longer connected now — Meta, GA4, Google Ads, Klaviyo, Microsoft Ads, Segment, TikTok, Pinterest or Attentive. The “why this score” list shows which one. A destination has to disappear from both the audited list and the live connection list before it counts, so a half-finished sync cannot trigger this on its own.
Competitor install detected −3 A competitor product is active on the store. Triggered by any competitor check naming one at Info, Warning or Error — see the list below. A competitor is a competitor however we spotted it, so the surface makes no difference and neither does the severity: the checks that only ever find an install cap themselves at Info, because Littledata cannot fix the fact that the merchant bought something else. The competitor’s name is included in the explanation.
App embed not enabled −4 Every Littledata client-side tracking app embed check that actually ran came back false (appEmbedEnabled, appEmbedEnabledStandardApp, appEmbedEnabledSegmentApp, appEmbedEnabledFreeApp). They are not tracking client-side events. A check that never ran is not evidence: a store with no embed check row is left alone, and so is one whose check threw in ld-shopify (a failed run still writes status: false, which is most of the false embed rows fleet-wide — Customer.shopifyAuditCheckErrors records which).
MonitorAI error −2 The store has at least one MonitorAI audit check at Error. The audits have been refined so an Error is taken as a real problem on its own — there’s no manual gate any more.
MonitorAI warning −1 Same, but the worst audit on the store is a Warning (no Errors). Stops there: Info, Success, and Unknown audit statuses don’t move the score.
Stale Linear ticket −1 each, capped at −3 Every Linear ticket on the store that is still unresolved, was opened more than 7 days ago, and was opened within the last 90 days. Read live from Linear (the same two links GET /customers/{shop}/linear-issues uses: the store’s Linear customer record, and CST issues naming the shop). MonitorAI’s own tickets don’t count — they come from audit checks that are already scored as MonitorAI error / warning, and charging a store twice for one finding is double-counting. A ticket raised yesterday is work in progress, not a signal; one raised a year ago is Linear housekeeping. The cap stops a bad support week outweighing every other signal on the account. If Linear can’t be read, the penalty is 0 — “we couldn’t ask” is not evidence of a problem.

The MonitorAI penalty is recalculated from the current audit-check rows on every refresh — fixing the underlying issue (so the audit moves back to Success / Info / Unknown) is what clears it.

How audit checks feed the score

Audit checks feed the score in two ways. Both apply at the same time — they’re independent signals — so a single audit can move the score twice (once via competitor detection, once via the worst-status penalty).

1. The competitor checks → Competitor install detected (−3)

Audit check (system name) Display name in cip Reads
klaviyoActiveCompetitorFlows Active alternative flows Klaviyo flow triggers
googleAdsCompetitorConversions Alternative server-side conversions Google Ads conversion action names
metaCompetitorCAPI Alternative CAPI detected Sibling pixels on the Meta dataset
shopifyAlternativeTracking Alternative server-side tracking The storefront’s own tags and web pixels
alternativeProperty Alternative property GA4 properties named after a rival

The list lives in one place, competitorChecks in models/auditChecks.ts; anything that writes competitorNames belongs on it.

When any of these is at Info, Warning or Error, the Competitor install detected −3 penalty fires once for the store, regardless of how many competitor checks are naming someone. Competitor names from the checks appear in the explanation.

Info counts here and nowhere else in the score. A competitor install is a commercial fact rather than a defect, so the storefront scan grades it Info and reserves Warning for a destination Littledata and the rival both feed — the half we can fix. Reading Error/Warning only threw away the widest detector we have.

These checks never open a Linear ticket (linearSuppressedChecks). There is nothing for us to fix, so every ticket raised on one ended up Canceled — and a Canceled ticket makes syncLinearState force its check back to Success, which used to erase the competitor from the account permanently. The penalty therefore ignores Linear state entirely: there is no ticket left to read around.

2. All audit checks → MonitorAI error / warning

Every other audit check the store has — EMQ, conversion-window drift, dataset quality, Klaviyo flow coverage, the same competitor checks, anything else — feeds the MonitorAI error / warning factor based on the worst single status across the whole catalogue. Error wins over Warning; Info / Success / Unknown don’t count.

This is a single signal per store, not per audit: a store with five Errors and twenty Warnings still scores −2 (one MonitorAI error penalty), not −10. The point of the penalty is “something on this store is at Error / Warning right now,” not “tally the misses.”

Worked example

A Plus store with Meta CAPI + Google Ads + Klaviyo + GA4 connected, a champion linked, no recent positive call, no open issues. Google Ads bids on the Littledata action, the Klaviyo flows are live, Google Ads returns 14×, and Bot Protection is on for Klaviyo:

+3 +1 +1 +1 +1 +1 +1 = +9 → 🟢 Green.

Now an Error appears on Active alternative flows (Klaviyo competitor) and nobody from the store has logged in for 45 days. The same audit feeds two factors — the specific competitor pathway and the catch-all worst-audit pathway:

+9 −3 −2 −1 = +3 → slides to the bottom of 🟢 Green.

The champion then leaves and their Champion? attribute is unset:

A destination is removed, and two support tickets the merchant raised a fortnight ago are still open:

0 −2 −2 = −4 → still 🔴 Red, deeper.

Where it shows up

When the score refreshes

The overnight refresh works through the base one store at a time and takes about an hour, so a score you see first thing in the morning is from last night, not from this minute. The “last refreshed” date on the customer detail page is the one to trust: if it is older than yesterday while other stores have moved on, that store’s refresh did not complete and the score on screen is stale — worth flagging rather than acting on.

If a score looks wrong, the most common explanations are:

  1. Champion left but still scoring −4 — the penalty reads the Champion contact tag. Untag the person who left (and tag their successor) in Intercom or from the Contacts tab in cip.littledata.io; the next nightly run clears it. Internal @littledata.io contacts are ignored everywhere.
  2. Champion left when nobody has left — the check asks about the customer as a myshopify slug plus a storefront domain, and LinkedIn answers with the employer it knows. Where a store is one brand in a group, those differ: beautyboxok is OK! Beauty Box, a Reach plc title, so its @reachplc.com people all read as departed. Noise in the slug does the same (jot-dev becomes the company name “jot dev”, which does not match “Jot”). Since #785 a moved verdict is ignored when the observed employer matches the contact’s own work-email domain, the storefront domain or the slug (services/contacts/employerIdentity.ts), and the check records reinterpretedFrom: "moved" so you can see it happened. Rows flagged before that guard existed keep the flag until their next verify pass — npx tsx scripts/reconcile-false-departures.ts --apply clears them in one go.
  3. Champion not detected — the Champion? attribute on the Intercom company isn’t set, or isn’t set to a truthy value (Yes / true / 1). Set it on the company, not on a contact.
  4. Login penalty still on — the login signal reads lastSeenAt only from contacts tagged Champion in Intercom. If nobody on the account carries that tag, you’ll see no login penalty but also no champion benefit; if a contact does carry it but hasn’t logged in, the −1 / −2 will apply until they do.
  5. Old MonitorAI penalty hanging around — it comes from the underlying audit checks. Fix the underlying issue so the audit re-runs at Success / Info / Unknown, and the next overnight refresh removes the penalty automatically.
  6. Competitor penalty they say is wrong — a competitor penalty clears only when the check stops naming one, which means the next audit run has to not find it: Info is enough to keep it, so “it’s only Info” is not the explanation. Cancelling a Linear ticket does nothing here, by design — that used to clear the competitor off the account for good. Open the named check on the customer page to see what it read, and if the detection itself is wrong, that is a bug in the check.
  7. Store isn’t scored at all — they’re probably missing one of the three prerequisites at the top of this page (a paid plan, Intercom company linked, at least one destination connected). The exception is a “Recent positive call” toggle, which forces a fresh score even without a connected destination.