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
Compare users who upgraded last quarter vs users who stayed flat. What pre-upgrade behaviors are highest signal?
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 }
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 }
]
}
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 integrationteam_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?”