Customer.io churn prevention: trigger on the usage drop, not the cancel click

By·Published·Updated
Customer.io churn prevention: trigger on the usage drop, not the cancel click

In 1926 Harry Scherman, an advertising copywriter, founded the Book-of-the-Month Club with two partners and an initial investment of forty thousand dollars (Encyclopedia.com). A panel of five judges picked each new book. Subscribers committed to buying a number of books over time, and the month's selection arrived in the mail. The club could rely on future sales before the titles were even announced.

Later, members could exercise a "negative option" and decline the month's book. Wikipedia describes the mechanism plainly: members chose through a reply card, and if they did not reply, they were shipped the Main Selection (Wikipedia). The default did the selling. The one thing a member had to send actively was a refusal.

Most subscription software still works this way. Renewal is the default, and the cancel click is often the first thing a customer actively tells you. By then they have decided.

TL;DR: A save email sent after someone clicks cancel arrives after the decision. The churn flows that keep customers fire earlier, when usage falls below that account's own normal level. In one preprint, 6.5 years of an academic publisher's subscription data, download volume and frequency predicted churn nearly a year ahead (Roberts et al., arXiv). Customer.io event conditions take one count and one window for everybody, so to hold each account to its own history, compute the baseline in your warehouse and send it as attributes. For B2B accounts held as custom objects, send a status word too: object triggers take operators such as "equal to" and "exists", not greater than or less than. Then keep a holdout. Per the AMA's award committee, Eva Ascarza found targeting by offer sensitivity cut churn 4.1 to 8.7 percentage points compared with targeting by risk (AMA).

Why the cancel click is the wrong trigger

The cancel page is where a customer acts on a decision. Whatever led to it happened earlier, and much of it leaves a trace in usage: fewer sessions, less of the core feature, no new colleagues invited. A flow triggered at the cancel click negotiates with someone who has already decided.

Research supports the order of events, if not a universal timetable. In a preprint posted to arXiv on 18 Nov 2022, Michael Roberts and colleagues studied 6.5 years of subscription data from a major academic publisher (arXiv). They describe it as a market that shares similarities with business-to-business. Their models used one thing: how much and how often each customer downloaded content. That was enough to predict churn nearly a year ahead, and usage sampled over time beat fine-grained behavioural detail. So usage is a strong signal, and in that study a simple measure of it was enough.

How far ahead your own customers drift is a number nobody else can give you. Take last year's churned accounts, line up their weekly usage by weeks before cancelling, and look for where the curve bends. That lag is your window.

Keep the cancel flow and the subscription centre. Payment failures need their own dunning flow, because nobody chose to leave. This post is about the earliest line of defence: the customer who still pays but has stopped using what they pay for. For the wider programme, see our SaaS retention playbook.

What usage event should you send to Customer.io?

Send one event for the action that delivers the value customers pay for. Not a login: a login is the door; the core action is the room. For a reporting tool, a report exported. For a scheduling product, a booking made. For a design tool, a file shared.

{
  "id": "ULID_GENERATED_WHEN_THE_EXPORT_HAPPENS",
  "name": "report_exported",
  "data": {
    "account_id": "acc_example",
    "report_type": "weekly_summary"
  }
}

Include account_id even in B2C, so your warehouse can roll usage up by account. Our event schema guide covers naming and properties.

Three details from Customer.io's events page shape how you send it.

  • Deduplicate. Send an id with each event, an uppercase ULID on the Track API, and Customer.io ignores a repeat for 30 days. Without one, a retried request that lands twice inflates the exact number you are watching.
  • Mind backfills. Events with a timestamp in the past 72 hours can trigger automations. Load history with care on launch day: anything dated in that window can fire flows you never meant to send.
  • Old events still count. Events older than 30 days leave profile activity but stay available for segment conditions.

Can Customer.io segments detect a usage drop on their own?

Partly. Segments count events in a window and find people who have gone quiet. They cannot compare a person with their own past.

What segments can do

Event conditions take a count and a time frame. The events page gives the example of an event that "has been performed at least once in the past 90 days" (events). The getting-started guide describes people who saw the same offer "five times in the past week" (segments and people data).

That gives you two patterns without any new data:

  • Gone silent. Has not performed report_exported within the past 30 days.
  • Was regular, now silent. Performed report_exported at least a set number of times within the past 30 days, and not within the past 7 days. Every qualifying use happened earlier in the month, so this catches regulars who stopped.

Customer.io's pre-made "Have not logged in recently" segment follows the same idea. It matches people who have been in your workspace for at least 30 days but have not viewed pages in the past month (how segments work).

The caveat in the negative condition

Has not performed within the past X days carries a condition the builder does not show. People are only evaluated if they have existed in Customer.io for at least X days, or performed an event before then (past X days help). The documented fix is an OR condition that catches newer people separately.

For churn that is mostly fine: a two-week-old account is an onboarding problem. It bites at launch. Import your customers today and nobody qualifies for a 30-day silence condition for a month, unless you back-date their earliest event.

One more trap: a trigger cannot rest on a not in segment alone (the not-in rule), so silence has to be the in side.

What segments cannot do

Every event condition holds one count and one window for everybody in the segment. A threshold that suits a light user never fires for a heavy user who falls from dozens of exports a week to a handful. That heavy user is often the account you most want to keep.

To catch a relative drop, Customer.io needs a number it does not hold: this account's normal level. So you compute it and send it.

How do you compute a per-account baseline?

Compute it in your warehouse or backend once a day, and send the results as attributes. Customer.io acts on numbers well; it is not where a rolling average should live.

Attribute What it holds
usage_last_week Core events in the last seven days
usage_baseline_weekly Average core events per week over the four weeks before that
usage_ratio Last week divided by the baseline

A worked example, as arithmetic rather than a measurement: an account with a baseline of 40 exports a week that logged 15 this week has a ratio of 0.375. Anything under one is below that account's normal.

Then add the attribute that does the real work: a usage_status word, set by the same job.

  • dropping: the ratio stays below about half for two weekly runs in a row.
  • dormant: the count is zero for four weekly runs in a row. An account always passes through dropping first.
  • healthy: everything else.
  • Unset: the baseline is too small to mean anything.

The persistence rule matters more than the exact cut-off. One bad week is a holiday; two in a row is a pattern. Let your data set the thresholds: compare the ratios of accounts that churned with those that renewed, and cut where they separate.

Getting the numbers into Customer.io

Customer.io's reverse ETL runs a query against your warehouse on a schedule, as often as every minute. Each row becomes an API call, such as identify for a person or group for an object. Filter on the last_sync_time variable so each run sends only rows that changed.

Also send an event when the status changes, such as usage_status_changed with the old status, the new one and the numbers. Attributes hold the current state for triggers, filters and exits. The event keeps the history: how often an account dips and recovers. Our post on attributes vs events explains why a changing value usually wants both.

Inactivity segment or relative baseline?

Situation Plain inactivity segment Per-account baseline
Usage similar across customers Enough Optional
Usage varies widely by size or plan Misses heavy users who slow down Needed
No warehouse or data team The practical choice Wait
B2B, several users per account Per person only Needed at account level
Seasonal or lumpy usage Use a long window Use a longer baseline

How should the trigger be designed?

Trigger on the status, not the raw number.

For people. Use the Attribute or Segment trigger with two profile attribute conditions joined by At least one: usage_status equal to dropping, or equal to dormant (Attribute or Segment trigger). Both are needed. With dropping alone, a customer who keeps sliding becomes dormant, stops matching and leaves the flow when they need it most. If you prefer the event, trigger on usage_status_changed with a data filter, and set the frequency to once within a time period so a flapping status cannot send twice.

Frequency. "Every re-match" means a person must stop matching and match again, after exiting the automation and after the minimum wait time. Customer.io's own feature-adoption recipe uses every re-match after a minimum of 30 days. One rescue attempt a month is a sensible ceiling here too.

Exit. By default, a person continues until they stop matching the trigger or filter conditions (exit conditions). With the two-condition trigger, that default does the right thing: an account that recovers to healthy leaves, and one that slides to dormant stays. Keep it. Our exit conditions audit covers the ways exits go wrong.

Freeze the numbers. The daily job updates the profile while someone is mid-flow. If an email says you exported 15 reports last week, copy that figure into a journey attribute at entry. Journey attributes take profile or trigger values, work in Liquid and branch conditions, and expire on exit (set journey attributes). Our journey attributes post shows the pattern.

The copy. Only now do you write anything. The first email should help with the thing they stopped doing: a shortcut, a template, a call with someone who can fix their set-up. Save the discount for later, if at all.

What changes for B2B accounts?

In B2B, churn is an account decision. One power user going quiet matters less than the whole account sliding. Customer.io's custom objects hold that account, and our custom objects guide covers the set-up.

Put the usage attributes on the account object with a group sync from the same job. Then trigger on Account updated, and you meet the constraint that shapes the design. The object updated trigger takes operators like equal to or exists. For anything else, the docs suggest a segment filter, or passing your data so the existing operators work. They are blunt about the gap: you "cannot use operators like greater than, less than" (object trigger operators).

The status word takes the second route, and it is the cleaner one here. The trigger fires on the change itself, Account updated where usage_status is equal to dropping, while a filter only narrows who enters. Account updated where usage_ratio is less than half does not work.

Plan around these details from the triggers and objects in automations pages.

  • Audience. Send to certain people related to the account. The docs' own example filters on a relationship role equal to admin. Message the admin or owner, not every seat.
  • Fan-out. One object update cannot reach more than 1,000 people.
  • Duplicates. A person related to several qualifying accounts enters once per account, so consultants with many client accounts can get several emails.
  • Checked once. Object trigger conditions are evaluated only before a person enters, so an admin carries on after the account recovers. Before each email, add a true/false branch on the triggering account's usage_status (the Trigger object condition) and end the flow unless it is dropping or dormant.
  • Re-entry. Object triggers support only every re-match, and an account that matches again can start a simultaneous journey for the same admin. The two-run persistence rule is the guard against flapping.
  • Read-only. Workflow actions can read object data but cannot update it, so the status logic stays in your job.

Per-seat usage belongs on the relationship. The feature-adoption recipe stores last_shared on each person's relationship to the account. The same idea lets an admin email name the colleagues who have stopped using the product, which beats usage is down.

When is a simple inactivity segment enough?

A silence segment is enough when usage is broadly similar across customers, when the product is used weekly or less, and when you have no warehouse yet. It is also the right first step everywhere: ship was regular, now silent this month, then add the baseline once you know what it misses.

Should you message the riskiest accounts first?

Not automatically. The accounts most likely to churn are not always the ones a message will save.

Eva Ascarza's paper "Retention Futility: Targeting High-Risk Customers Might be Ineffective" appeared in the Journal of Marketing Research in February 2018. It ran two field studies, at a subscription membership organisation and a wireless telecoms firm. The AMA's award committee summarised the result: targeting by sensitivity to the offer, rather than churn risk, reduced churn by 4.1 to 8.7 percentage points. In some instances, targeting the customers most likely to leave increased churn (AMA). The paper won the AMA's 2023 Weitz-Winer-O'Dell Award.

For a usage-drop flow, do not assume dormant accounts deserve more emails than dropping ones. One plausible reason, our reading rather than the paper's: a reminder of an unused subscription can prompt the cancellation it was meant to prevent. Either way, keep a holdout.

How do you know the flow is working?

Hold some accounts back and compare. A random cohort branch splits people, and its "Cohort by" setting sends everyone related to the same object down the same path (objects in automations). No account ends up half in the test and half in the control. Our post on the holdout checkbox covers measurement.

Measure two outcomes, against the holdout:

  1. Recovery. Did usage_status return to healthy within a few weeks?
  2. Retention. Did the account renew?

A flow that lifts recovery but not retention is buying logins, not keeping customers. A cohort view by trigger month tracks both.

If you would like help wiring a usage event and baseline into your Customer.io workspace, send us a note.

FAQ

Can Customer.io trigger a churn email when usage drops?

Yes, if you send Customer.io a usage event and, for relative drops, a baseline. To compare each account with its own normal level, compute the baseline upstream, send it as attributes and trigger on a status attribute.

Why is a cancellation save email too late?

A save email at the cancel click reaches people who have already decided to leave. Usage carries the warning much earlier: one study of 6.5 years of a publisher's subscription data predicted churn nearly a year ahead from download volume and frequency alone.

Why don't new customers match an inactivity segment in Customer.io?

Customer.io only evaluates negative "within the past X days" conditions for people who have existed in the workspace for at least X days, or who performed an event before then. The documented workaround is an OR condition that catches newer people separately.

Can a Customer.io segment compare someone's usage with their own past usage?

Not by itself, because each event condition takes one count and one time window for everyone in the segment. Compute a per-person or per-account baseline outside Customer.io, send it as an attribute, and segment on that.

How do I trigger a churn flow for a whole B2B account in Customer.io?

Store the account as a custom object with its usage numbers, plus a status word such as dropping. Object updated triggers take operators such as "equal to" and "exists", not greater than or less than, so trigger on the status and check it again before each email. Then send to related people whose relationship role is admin.

Should I send the usage drop as an attribute or an event?

Send the usage drop as both an attribute and an event. Attributes such as usage_status hold the current state for triggers, filters and exits, and an event such as usage_status_changed records each change so you can count dips and recoveries.

Should churn emails target the customers most likely to cancel?

Not automatically: Ascarza's "Retention Futility" research found that targeting customers by their sensitivity to the offer reduced churn by 4.1 to 8.7 percentage points compared with targeting by risk. Keep a holdout so you can see which accounts your flow actually saves.

Sources

Want this working in your Customer.io workspace?

It's what we do all day for SMBs—strategy, automations, deliverability and hands-on execution.

See how we work as a Customer.io agency →
David Crowther
Book a free consultation →

On our call, you'll be speaking with David Crowther, founder of NerveCentral.

Our initial consultation is not a sales call—you'll talk, I'll listen and ask questions—then I'll come back to you within 48 hours with our best ideas on how to grow your business.