What behaviors predict an account will upgrade?

Compare upgraders vs flat users from last quarter. Find the pre-upgrade behaviors that most strongly predict expansion.

Published

difficulty advanced · time to value 10 minutes · tools used 2

Just say this

Compare users who upgraded last quarter vs users who stayed flat. What pre-upgrade behaviors are highest signal?

Why this matters

Every PLG team wants the inverse of activation: “what behaviors predict an account will UPGRADE?” The activation-moment recipe asks “what makes them stick;” this asks “what makes them spend more.”

The mechanics are identical. Build two cohorts (upgraders vs flat-stayers from last quarter), compute behavior in the pre-upgrade window, find the events with the largest lift in the upgrader group. The output is a leaderboard of “if a user does X, they’re 4x more likely to upgrade.”

PostHog can do this — it’s called “correlation analysis” — but the UI requires you know which event you suspect. The agent doesn’t need a hypothesis; it scans every event.

What you get

  • A list of events ranked by upgrade-lift score (P(event | upgraded) / P(event | flat))
  • The top 5 predictive events, each with a confidence band
  • A suggested in-app nudge: “users who do X are 4x more likely to upgrade — surface X in onboarding”

Walk through it

You

Compare users who upgraded last quarter vs users who stayed flat. What pre-upgrade behaviors are highest signal?

Agent

Let me build two cohorts: upgraders (account did plan_changed upward in the last 90 days) and flat-stayers (paid throughout, no plan change).

agentry_create_cohort {
  project_id: "default",
  name: "upgraders_q1",
  description: "Accounts that upgraded plan_rank during the last 90 days",
  filters: [
    { event: "plan_changed", to_plan_rank_gt: "from_plan_rank", since: "90d" }
  ]
}
{ "cohort_id": "coh_up_1", "member_count": 64 }
agentry_create_cohort {
  project_id: "default",
  name: "flat_stayers_q1",
  description: "Accounts that were paid throughout last 90d and did not upgrade",
  filters: [
    { event: "subscription_started", before: "90d ago" },
    { event: "plan_changed", since: "90d", absent: true }
  ]
}
{ "cohort_id": "coh_flat_1", "member_count": 318 }
Agent

64 upgraders vs 318 flat. Now compute event frequency in the 30-day window BEFORE upgrade for upgraders, vs a matched 30-day window for flat-stayers.

agentry_analytics_query {
  project_id: "default",
  hogql: "WITH upgraders AS (SELECT properties.account_id AS aid, min(timestamp) AS event_ts FROM events WHERE event = 'plan_changed' AND properties.to_plan_rank > properties.from_plan_rank AND timestamp >= now() - INTERVAL 90 DAY GROUP BY aid), up_events AS (SELECT e.event AS event_name, count(DISTINCT e.distinct_id) AS upgrader_users FROM events e JOIN upgraders u ON e.properties.account_id = u.aid WHERE e.timestamp >= u.event_ts - INTERVAL 30 DAY AND e.timestamp < u.event_ts AND e.event != '$exception' GROUP BY event_name), flat_events AS (SELECT event AS event_name, count(DISTINCT distinct_id) AS flat_users FROM events WHERE properties.account_id IN (SELECT properties.account_id FROM events WHERE event = 'subscription_started' AND timestamp < now() - INTERVAL 90 DAY) AND timestamp >= now() - INTERVAL 60 DAY AND timestamp < now() - INTERVAL 30 DAY AND event != '$exception' GROUP BY event_name) SELECT u.event_name AS event, u.upgrader_users AS upgrader_n, f.flat_users AS flat_n, round(u.upgrader_users * 1.0 / 64 / nullIf(f.flat_users * 1.0 / 318, 0), 2) AS upgrade_lift FROM up_events u LEFT JOIN flat_events f ON u.event_name = f.event_name WHERE u.upgrader_users >= 10 ORDER BY upgrade_lift DESC LIMIT 20"
}
{
  "rows": [
    { "event": "api_key_rotated",            "upgrader_n": 28, "flat_n": 18,  "upgrade_lift": 7.7 },
    { "event": "webhook_created",            "upgrader_n": 41, "flat_n": 42,  "upgrade_lift": 4.8 },
    { "event": "team_member_invited",        "upgrader_n": 52, "flat_n": 78,  "upgrade_lift": 3.3 },
    { "event": "report_exported",            "upgrader_n": 38, "flat_n": 88,  "upgrade_lift": 2.1 },
    { "event": "integration_slack_connected","upgrader_n": 32, "flat_n": 88,  "upgrade_lift": 1.8 },
    { "event": "dashboard_view",             "upgrader_n": 64, "flat_n": 318, "upgrade_lift": 1.0 }
  ]
}
Agent

Headline: rotating an API key is a 7.7x upgrade predictor. That’s surprising — but makes sense. Teams that rotate API keys are in production, treating you as critical infra, and feeling limits. They’re who you want to convert.

Other strong signals:

  • webhook_created (4.8x) — they’re operationalizing the integration
  • team_member_invited (3.3x) — collaboration, the perennial upgrade trigger

Suggested in-app nudge: when a user rotates an API key, offer a “production-ready guide” CTA that includes the upgrade path. They’re the most warm lead you have.

The output

Upgrade Lift Analysis (last 90d)

EVENT                          UPGRADER  FLAT   LIFT
api_key_rotated                     28    18   7.7x   ★ surprising winner
webhook_created                     41    42   4.8x   ★ operationalizing
team_member_invited                 52    78   3.3x
report_exported                     38    88   2.1x
integration_slack_connected         32    88   1.8x
dashboard_view (baseline)           64   318   1.0x

INTERPRETATION:
  Rotating an API key = treating you as production infra. These users are
  the warmest upgrade leads you have. Trigger an in-app nudge or sales
  outreach on this event.

SUGGESTED EXPERIMENTS:
  1. Show "production checklist" + upgrade CTA after first api_key_rotated
  2. After webhook_created → email "your webhook is live, here's how teams
     scale this"
  3. Make team-invite a first-class onboarding step (currently buried)

CAVEATS:
  - n=64 upgraders is small. Re-run quarterly as the cohort grows.
  - Lift != causation. Could be that mature teams do BOTH "rotate keys"
    AND "upgrade" naturally. Treat as signal, not proof.

Setting it up

If you’ve already set up the cohorts and account-id-on-everything pattern from account-health-score / tier-downgrade, you have what you need. The recipe asks the agent to join plan_changed events back against the per-account event history.

If you don’t yet have plan_changed with rank metadata:

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

await fetch(`https://api.agentry.sh/v1/analytics/${process.env.AGENTRY_PROJECT_ID}/`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.AGENTRY_DSN}`,
    "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: {
      account_id: subscription.metadata.account_id,
      from_plan: oldPlanName,
      to_plan: newPlanName,
      from_plan_rank: PLAN_RANK[oldPlanName],
      to_plan_rank: PLAN_RANK[newPlanName],
    },
  }),
});

The 90-day default window is reasonable for SaaS; if your sales cycle is shorter (PLG with weekly upgrades), drop it to 30d for faster signal.

Variations

  • “Same analysis but for SELF-SERVE upgrades only. Sales-led deals confound the signal.”
  • “For the top predictive event (api_key_rotated), show the distribution: when do users first do it relative to signup?”
  • “Build the inverse — what behaviors predict DOWNGRADE? Should I worry about cohorts doing X?”
  • “Compare upgrade-lift across plan tiers — different signal for free→starter vs pro→scale?”

Try this recipe in your own agent.

Paste the prompt above into your agent. It'll set up Agentry against your data, then run the recipe and show you the real output.

Install https://agentry.sh/agentry.md and set it up