Why did this account drop from pro to free?

Find accounts that downgraded in the last 30 days and compare their behavior before vs after. The soft-churn signal nobody tracks.

Updated · published

markdown for agents →

difficulty intermediate · time to value 5 minutes · execution on demand

Start from this

List paid accounts that downgraded in the last 30 days. For each, compare their behavior in the 30 days before vs after the downgrade.

Why this matters

Downgrade is the soft-churn signal nobody tracks. Cancellation gets a workflow (exit survey, save-offer email, CS call). Downgrade just happens silently in the Stripe dashboard — they stop paying you $300/mo and start paying you $0/mo, and you find out next quarter when MRR breaks down.

But downgrade is the highest-quality leading indicator there is. Saving a downgrade is 10x cheaper than winning back a full cancellation (they still have the account, the data, the muscle memory). The reason for the downgrade is usually small and addressable — they stopped using one specific feature, or their team shrunk and didn’t need 10 seats.

Mixpanel and PostHog can do before/after cohort comparisons but you have to build them by hand each time. Agentry collapses it to one prompt: “show me last month’s downgrades and what changed in their behavior.”

What you get

  • A list of accounts that downgraded in the last 30 days, with from_plan → to_plan
  • For each: top-3 events that decreased the most between the 30-day pre and post window
  • A CS-ready summary: “Acme dropped Pro → Free; they stopped using the API integration 2 weeks before downgrade”

Walk through it

You

List paid accounts that downgraded in the last 30 days. For each, compare their behavior in the 30 days before vs after the downgrade.

Agent

Let me first identify the downgraders.

Endpoint: POST /v1/projects/:project_id/analytics/query
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fanalytics%2Fquery&method=post
Custom HogQL goes in the OpenAPI-defined `query` field.
Concept fields:
  query: "SELECT distinct_id, properties.from_plan AS from_plan, properties.to_plan AS to_plan, timestamp AS event_ts FROM events WHERE event = 'plan_changed' AND properties.from_plan_rank > properties.to_plan_rank AND timestamp >= now() - INTERVAL 30 DAY ORDER BY event_ts DESC"
{
  "rows": [
    { "distinct_id": "team-acme",     "from_plan": "pro",        "to_plan": "free",    "event_ts": 1747100000 },
    { "distinct_id": "team-globex",   "from_plan": "scale",      "to_plan": "pro",     "event_ts": 1746950000 },
    { "distinct_id": "team-megacorp", "from_plan": "enterprise", "to_plan": "scale",   "event_ts": 1746800000 },
    { "distinct_id": "team-tinyco",   "from_plan": "starter",    "to_plan": "free",    "event_ts": 1746700000 }
  ]
}
Agent

Four downgrades. Let me put them in a cohort and pull behavior before/after for the largest one (team-acme, Pro→Free, lost $300/mo).

Endpoint: POST /v1/projects/:project_id/cohorts
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fcohorts&method=post
Concept fields:
  name: "downgrades_last_30d",
  description: "Accounts that dropped to a lower tier in last 30d",
  filters: [{ event: "plan_changed", to_plan_rank_lt: "from_plan_rank", since: "30d" }]
{ "cohort_id": "coh_dg_1", "member_count": 4 }
Endpoint: GET /v1/projects/:project_id/users/:distinct_id/summary
Exact shape: /v1/openapi.json?path=%2Fv1%2Fprojects%2F%7Bproject_id%7D%2Fusers%2F%7Bdistinct_id%7D%2Fsummary&method=get
Use query parameters, not a JSON body.
Query/filter concept:
  windows: [
    { name: "pre_downgrade",  from: 1744508000, to: 1747100000 },
    { name: "post_downgrade", from: 1747100000, to: 1749692000 }
  ]
{
  "windows": {
    "pre_downgrade": {
      "total_events": 1840,
      "by_event": {
        "api_call": 1240,
        "dashboard_view": 480,
        "report_exported": 88,
        "team_member_invited": 12,
        "integration_slack_used": 20
      }
    },
    "post_downgrade": {
      "total_events": 220,
      "by_event": {
        "api_call": 4,
        "dashboard_view": 200,
        "report_exported": 12,
        "team_member_invited": 0,
        "integration_slack_used": 4
      }
    }
  }
}
Agent

Acme’s downgrade tells a clear story:

  • API calls collapsed from 1,240 → 4 (-99%). They were Pro because they needed the API. They stopped using it 30 days before the downgrade.
  • Dashboard views barely changed (480 → 200, which is half because the time window is 30 days vs 30 days but they’re now visiting less). They’re still around.
  • Team invites: 12 → 0. Team shrunk.

Likely reason: an engineering project ended, they don’t need the API right now, they want to keep their data so they downgraded instead of canceling. This is a winback candidate, not a churn candidate. A “want to come back to Pro for $99 first month?” email would probably land.

Want me to do the same analysis for the other three?

The output

Downgrades (last 30d) — pre vs post behavior analysis

team-acme         Pro → Free       ($300/mo lost)
  ↓ api_call:               1240 → 4    (-99.7%)  ★ THE STORY
  ↓ team_member_invited:      12 → 0    (-100%)
  ↓ dashboard_view:          480 → 200  (still showing up)
  ↓ report_exported:          88 → 12
  Diagnosis: API project ended. Saving the seat = saving Pro.
  Recommended outreach: "API back for $99 first month?" save offer

team-globex       Scale → Pro      ($1,800/mo retained, $1,200/mo lost)
  ↓ workflow_executed:    14,200 → 4,100  (-71%)
  Diagnosis: workflow volume cut. They downsized correctly.
  Action: NO outreach — they're happy at Pro.

team-megacorp     Enterprise → Scale  ($27k/mo lost)  ★ URGENT
  ↓ admin_action:        220 → 12   (-95%)
  ↓ team_member_invited:  24 → 0
  Diagnosis: champion left? Admin activity collapsed.
  Recommended outreach: CSM calls TODAY, find the new champion.

team-tinyco       Starter → Free   ($60/mo lost)
  ↓ all events:           220 → 18   (-92%)
  Diagnosis: Dormant. Probably hobby project ended.
  Action: NO outreach (low ROI), let them stay on free.

Setting it up

The workflow needs one event class — plan_changed — fired from your Stripe webhook handler whenever a subscription changes tier. Include both from_plan and to_plan, and a numeric plan_rank so the agent can detect “down” without hard-coding tier names:

// In your Stripe webhook handler for `customer.subscription.updated`
const PLAN_RANK: Record<string, number> = {
  free: 0,
  starter: 1,
  pro: 2,
  scale: 3,
  enterprise: 4,
};

if (oldPriceId !== newPriceId) {
  const from_plan = planNameFromPrice(oldPriceId);
  const to_plan = planNameFromPrice(newPriceId);

  await fetch(`https://api.agentry.sh/v1/analytics/`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.AGENTRY_SERVER_API_KEY}`,
      "Content-Type": "application/json",
      "User-Agent": "myapp-stripe/1.0",  // REQUIRED — Cloudflare 403s default UAs
    },
    body: JSON.stringify({
      event: "plan_changed",
      distinct_id: subscription.metadata.account_id,
      properties: {
        from_plan,
        to_plan,
        from_plan_rank: PLAN_RANK[from_plan],
        to_plan_rank: PLAN_RANK[to_plan],
        mrr_delta: (newPriceCents - oldPriceCents) / 100,
      },
    }),
  });
}

For the before/after windows to work, you need at least 30 days of event history for each downgraded account. If you only started instrumenting last week, this workflow gets useful in ~3-4 weeks.

Variations

  • “For each downgrade, also show the SUPPORT tickets they opened in the 30 days before. Was there a known frustration?”
  • “Of the accounts that downgraded last quarter, how many fully canceled within 90 days? That’s our ‘downgrade → churn’ conversion rate.”
  • “Build the inverse — accounts that UPGRADED. What behaviors increased?” (see expansion-prediction)
  • “Review the previous week’s downgrades on demand and draft a diagnosis for a CS lead to share if appropriate.”

Adapt this workflow in your own agent.

Ask your agent to map the starter prompt to your saved signal map, live events, and the current HTTP API before answering.

Agentry gives coding agents production context from errors, product analytics, and deploys through one HTTP API. Fetch and read https://agentry.sh/install.md completely, then install Agentry's full first-time telemetry baseline in this repository. I authorize device-code login, saving the owner key only to ~/.agentry/credentials.json, creating one Agentry project for this repository, and read-only inspection of shipped source plus hidden CI/provider configuration. Deeply analyze the product's business logic and complete value flow before choosing telemetry. Save the complete source-backed plan and show me its exact contents and hash. Until I explicitly approve that plan or request a replacement, do not edit app/CI source, place runtime/CI credentials, commit, exercise product traffic, start proof, push, or deploy. After approval, I authorize only the reviewed targets: place the required scoped browser/server/CI credentials through the established environment or secret mechanism, preserve existing telemetry, implement and test the baseline, commit it, push that reviewed commit when the shipped CI/provider path requires it, exercise safe proof paths with test/non-customer data, and perform one deployment through the reviewed shipped CI/provider path. Ask first if proof would charge money, contact a third party, change real customer data, or require new external access. After the plan is saved, immediately before every onboarding state-changing POST, GET current onboarding state, perform only its single returned next_action, then read state again; do not batch or infer later stages. Continue until status is verified, installation_complete is true, and next_action is null. Keep all secrets, source snapshots, proof markers, and scratch files outside the repository.

+ Full access
5.5 Extra High
  1. 1. Open your repo in Codex, Claude Code, Cursor etc.
  2. 2. Paste the install prompt.
  3. 3. Your agent reads the install doc and shows you an implementation plan for approval.