# 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.

## Agent adaptation contract

- Canonical human page: https://agentry.sh/workflows/expansion-prediction
- Execution mode: on_demand
- Immutable automation template: none
- Applies to: b2b-saas, b2c-saas, devtools-api
- Required example events: subscription_upgraded
- Required Agentry resources: none declared
- Do not use when:
  - Do not use until the example events are mapped to observed project signals, the current onboarding state is verified, and live event/property reads prove the required data is present.
  - Do not recommend this for products without a real revenue lifecycle or an equivalent paid-account signal.
- Ask before using:
  - Which observed events map to subscription_upgraded? Is the current onboarding state verified, and do live event/property reads show non-synthetic traffic for them?

This is an adaptable workflow example, not an API recipe. Map event and property names to the project's saved signal map, require status: "verified" from GET /v1/projects/:project_id/onboarding, and confirm the required signals through live event/property metadata and rows. Fetch current OpenAPI or query-blueprint details before making calls. Do not infer unattended authority from this page.

## Why this matters

Every PLG team wants the inverse of activation: "what behaviors predict an account will UPGRADE?" The activation-moment workflow 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).

```text
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: "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" }
  ]
```
```text
{ "cohort_id": "coh_up_1", "member_count": 64 }
```

```text
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: "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 }
  ]
```
```text
{ "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.

```text
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: "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"
```
```text
{
  "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 workflow 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:

```ts
// 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/`, {
  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: {
      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?"*
