Back to Blog

Ask AI Why Customers Cancelled (Using Your Real Database)

When weekly cancellations spike, ask Claude or Cursor to read the last N cancel rows — plans, last active, stored reasons — instead of guessing. Prompts, limits, and what the data can't tell you.

Weekly report says cancellations doubled. Someone asks why.

Without live data, people guess: pricing, a bug, “seasonality.” With a database connection, you can ask a narrower question: what do the last cancel rows actually contain?

Not a crystal ball — faster triage. Claude or Cursor can read plans, timestamps, and a reason column if you stored one, and should say when you didn’t.

Pairs with weekly database reports in Claude Code: when the churn line moves, dig in. Also: staging first, don’t open every table, verify answers.

What “why” can mean in SQL

Your schema usually supports some of these — not all:

| Signal in the DB | What you can ask | |------------------|------------------| | cancelled_at / status | Who and when | | Plan or price at cancel | Cheap vs expensive plans | | last_seen_at / last login | Already gone cold vs active-then-left | | cancel_reason / survey text | Literal reasons (messy but real) | | Linked tickets or NPS | Only if those tables exist and are allowed |

If the only fact is status = 'cancelled', a honest answer is: “We don’t store a reason; here’s tenure and plan distribution.” Anything more is fiction.

Prompt: last 20 cancellations

Read-only database access. Inspect schema first.

Find the last 20 customers or subscriptions that cancelled or unsubscribed
(use whatever tables/columns we actually have).

For each, pull only non-sensitive fields needed for patterns:
plan, cancelled_at, created_at or tenure, last_active if present,
cancel_reason if present.

Then summarize:
- common plans
- tenure bands (e.g. <7 days, 7-30, 30+)
- whether reasons exist and what they say
- anything that looks clustered in time

Rules:
- Do not invent cancel reasons
- If emails or raw message bodies appear in the table, do not print full PII — aggregate
- Show the SQL
- If we can't define "cancelled" from the schema, stop and say what's missing

Run it once manually. Check a couple of IDs in your admin UI or Stripe. Known-answer habit still applies.

Prompt: spike week vs prior week

After a weekly report flags a jump:

Cancellations look higher this week than last week.
Using the database, compare the two weeks:
- counts
- plan mix
- share with a filled cancel_reason
- median tenure at cancel

Call out differences with numbers. If sample size is tiny, say the comparison is weak.
SELECT only. Show SQL.

Prompt: “are cancels mostly trials that never activated?”

Among cancelled accounts in the last 90 days, what share never passed
a basic activation event we store (e.g. created a project, sent a first request)?
Define activation from real event/table names after inspecting schema.
If we don't log activation, say so — don't proxy with "logged in once."

Product teams ask this constantly. The model only helps if activation is in the DB you connected.

Limits (say them out loud)

  • No reason field → no mind reading. Store a reason, import Stripe’s cancellation feedback, or accept aggregate-only answers.
  • PII in chat. Cancel rows often include emails. Prefer aggregates, staging, or an allowlist that exposes an anonymized view.
  • Correlation ≠ cause. “Most cancels were on the $9 plan” is a fact. “Because of price” is a hypothesis — test it elsewhere.
  • Warehouse vs OLTP. Heavy churn history might live in the warehouse. If Postgres is thin, don’t force MCP onto the wrong store.

Where this fits in the week

  1. Monday job reports cancel count (weekly reports)
  2. If the line moves, run the “last 20” prompt
  3. If reasons are empty, fix instrumentation — don’t keep asking the model for a story
  4. Check Usage so you know which connection answered

Better question than “why do we lose customers?”: “what’s in the cancel rows we already have?”

Safe DB at mcpserver.design, restrict tables, paste the last-20 prompt.

Related reading: