CLI command
churn
Why subscribers leave and who is about to: cancellation survey answers counted by reason, and churn risk rules that flag people whose usage fell or whose billing failed and enrol them in a rescue sequence.
Invocation#
echo-cli churn reasons --days <days>
echo-cli churn risk list --product <id>
echo-cli churn risk add --product <id> --name <text> --app <web or mobile source> --baseline-days <n> --recent-days <n> --drop <fraction> --rescue <trigger-id> --by <name>
echo-cli churn risk remove --id <rule-id>
echo-cli churn risk run --product <id>
echo-cli churn risk flags --rule <rule-id>Inputs and output#
reasons writes since (the start of the window), cancellations (the RevenueCat cancellation events and the Stripe cancellations the webhook recorded in the window), answered (the survey answers given in it) and reasons, most given first, each with reason, answers and share_of_answered. risk list writes the product's rules by name: id, product_id, name, app_id, baseline_days, recent_days, drop_fraction, rescue_trigger_id, created_by and created_at; risk add and risk remove write the rule they made or removed. risk run writes runs, one per rule with rule_id, name, watched (the people with events in its days) and flagged (the flags it made). risk flags writes a rule's flags newest first: id, rule_id, subject, baseline_events, recent_events, series (each day with its events), billing_issues (each event_id and received_at), enrollment_id (the rescue enrolment) and flagged_at.
State effect#
churn reasons calls Echo Web's authenticated /api/analytics/churn/reasons with ECHO_INTERNAL_BEARER at ECHO_WEB_BASE_URL; the Churn reasons section of the tracking screen and of Echo Desktop's Contact frequency screen read the same counts, and echo-mcp reads them too (echo_churn_reasons). A subscriber answers after cancelling, signed in, with POST /api/public/cancellations/answers { billing, subscription_id, reason, detail }, billing naming where the subscription is billed (revenuecat or stripe) and detail being their own words or null. With revenuecat, subscription_id is the original transaction id and the answer is stored against the latest stored RevenueCat cancellation of that subscription (cancellation_event_id), only when that cancellation names the subscriber as its app user, original app user or an alias. With stripe, subscription_id is the Stripe subscription id and the answer is stored against the latest cancellation the Stripe webhook recorded for it as a web event (stripe_event_id) — recorded when Stripe deletes the subscription — only when that event names, as its user or anonymous id, an identifier the identity edges link to the signed-in user. Either way once per cancellation. Reasons are counted without regard to case, under the first spelling given. cancellations and answered cover the same days, so an answer to a cancellation stored before the window counts in answered only. The risk verbs call /api/analytics/churn/risk, /run and /flags; the Churn risk section of the tracking screen and of Echo Desktop's Contact frequency screen call the same operations, Echo Web runs every product's rules daily at /api/cron/churn-risk, and echo-mcp lists rules (echo_list_churn_risk_rules) and reads flags (echo_churn_risk_flags) but runs nothing. A rule reads its source's stored events — a web source's web events or a mobile app's mobile events — over --baseline-days followed by --recent-days ending now, joining every identifier the identity edges link into one person. A person with events in the baseline days is flagged when their daily rate in the recent days fell by at least --drop (a fraction above 0 and at most 1) of their baseline daily rate, or when a RevenueCat billing issue names one of their identifiers in the recent days; a person with no baseline events is never flagged for usage. Each person is flagged once per rule and enrolled once in the sequence of the --rescue lifecycle trigger (see lifecycle), which must belong to the same product; a trigger a rule rescues into cannot be removed until the rule is.
Refusals#
- A wrong invocation is refused before anything is sent: "churn takes <every usage, separated by semicolons>; got <word>", "churn <verb> does not take <flag>; it takes …", "<flag> needs a value" and "churn <verb> needs <flag>" for each flag the verb needs.
- Echo Web answers a bad request with "days is required: how many days of answers to count" or "days must be a positive whole number: the window to read". The public answer is refused with "sign in to answer: the answer belongs to the subscriber who cancelled" as unauthorized, "billing is required: where the subscription is billed, one of revenuecat, stripe", "billing must be one of revenuecat, stripe: where the subscription is billed; got <value>", "subscription_id is required: …", "reason is required: why the subscription was cancelled" or "detail is required: what the subscriber added, or null when they wrote nothing" as a bad request, "subscription <id> has no stored cancellation; a survey answers a cancellation" or "Stripe subscription <id> has no stored cancellation; a survey answers a cancellation" as not found, "the cancellation of subscription <id> names another subscriber; only they can say why it was cancelled" (or of Stripe subscription <id>) as forbidden, and "this cancellation was already answered at <time>" as a conflict. The command prints Echo Web's sentence and exits non-zero.
- The risk verbs are refused with "product_id is required: …", "name is required: …", "app_id is required: …", "baseline_days must be a positive whole number of days: …" (and recent_days), "drop_fraction is required: …", "drop_fraction must be above 0 and at most 1: the share of the usual rate the recent rate lost; got <n>", "rescue_trigger_id is required: …", "lifecycle trigger <id> belongs to product <id>, not <id>; a rule rescues into its own product's sequence", "rule_id is required: …" or "created_by is required: …" as a bad request; "no product has id <id>; echo-cli products lists the ones that exist", "no lifecycle trigger has id <id>; echo-cli lifecycle triggers list --product <id> lists them" or "no churn risk rule has id <id>; echo-cli churn risk list --product <id> lists them" as not found; and "product <id> already has the churn risk rule <name> as <id>" as a conflict. Removing a trigger a rule rescues into is refused with "lifecycle trigger <id> is the rescue of the churn risk rules <name (id), …>; remove them first with echo-cli churn risk remove".