Usage upsell
Offers the next Offer when a Customer’s use paces past its Limit, with today’s prorated charge; the Customer upgrades in one press.
- Channel
- More seats and upgrades
- Kind
- Conversion
- Summary
- startupbuilder.dev
Be our CSM: when a Customer is on pace to pass their plan’s limit, show them the next plan with today’s charge, or tell them staying is cheaper when it is. Over 150 units, bring in our AE. Show me what’s missing; don’t send.
# Not live yet (gaps #2)
curl -X POST "https://api.sb/leaseline.app/campaigns?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"usage-upsell"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'usage-upsell' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2
npx startup-builder do Campaign.create --json '{"play":"usage-upsell"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'usage-upsell' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2",
"mode": "try"
}
}application/json{
"type": "https: //startupbuilder.dev/ errors/ refused",
"title": "Refused",
"status": 422,
"detail": "usage-upsell can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "41 Customers, 6 a month pacing past a Limit",
"doc": "/plays/ usage-upsell#check-use-when",
"then": "Metric.query"
},
{
…
Hand our latest Customer near a limit to the CSM agent and show me what it chose and why. Don’t send.
curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Entitlement.checked%22%2C%20%22Offer.subscribed%22%2C%20%22Invoice.billed%22%2C%20%22Message.received%22%2C%20%22Renewal.opened%22%2C%20%22Subscription.switched%22%2C%20%22Page.viewed%22)&limit=1" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
# Not live yet (gaps #7)
curl -X POST "https://api.sb/leaseline.app/triggers/fire?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"like":"event_1204"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22customer_12%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/actions?where=on%20%3D%20%22customer_12%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Entitlement.checked", "Offer.subscribed", "Invoice.billed", "Message.received", "Renewal.opened", "Subscription.switched", "Page.viewed")', limit: 1 }) // { play } compiled to this Play’s Trigger Events
const { subject } = await $.Trigger.fire({ like: last.$id }, { mode: 'try' }) // the same Event again, in try mode · Not live yet, gaps #7
const { decisions: [decision] } = await $.Decision.list({ where: `on = "${subject}"` }) // what it chose, and why · Not live yet, gaps #7
const { actions } = await $.Action.list({ where: `on = "${subject}"` }) // what it did, with its checks · Not live yet, gaps #7
return { decision, actions }
npx startup-builder do Event.list --json '{"where":"name in (\"Entitlement.checked\", \"Offer.subscribed\", \"Invoice.billed\", \"Message.received\", \"Renewal.opened\", \"Subscription.switched\", \"Page.viewed\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1204"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"customer_12\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"customer_12\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Entitlement.checked\", \"Offer.subscribed\", \"Invoice.billed\", \"Message.received\", \"Renewal.opened\", \"Subscription.switched\", \"Page.viewed\")', limit: 1 }) // { play } compiled to this Play’s Trigger Events\nconst { subject } = await $.Trigger.fire({ like: last.$id }, { mode: 'try' }) // the same Event again, in try mode · Not live yet, gaps #7\nconst { decisions: [decision] } = await $.Decision.list({ where: `on = \"${subject}\"` }) // what it chose, and why · Not live yet, gaps #7\nconst { actions } = await $.Action.list({ where: `on = \"${subject}\"` }) // what it did, with its checks · Not live yet, gaps #7\nreturn { decision, actions }",
"mode": "try"
}
}application/json{
"decision": {
"$id": "https: //api.sb/ leaseline.app/ decisions/ decision_340",
"$type": "Decision",
"choice": "offer-next-plan",
"reason": "118 units on Pro: $68 of overage this period is more than the $50 step to Team",
"stakes": "low",
"options": [
"offer-next-plan",
"show-the-saving",
"at-renewal",
"to-the-ae",
"not-now"
],
…
sample · 4 Not live yetPreviewOn PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim
action_2 is the note to Kestrel’s billing owner, held 2 minutes; the in-product notice shows at their next sign-in.
$2,
| Try another: the Customer | Choice | Reason |
|---|---|---|
| Kestrel Rentals: 118 units on Pro, $68 of overage this period | offer-next-plan | overage passes the $50 step to Team |
| Maple Court: 56 units on Pro, $6 of overage | show-the-saving | staying on Pro costs less than Team |
| Harbor Row: 140 units, at risk since Oct 2 | not-now | keep-first: their CSM sees them first |
- 20 min week 1, then 5 a week
- Your time
- Day 4: first upgrade paid
- First signal
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Limit upgrade ARR → Upgrades ↑ → ARR added each week → ARR
Worth’s formula: about +18 upgrades per 100 pacing Customers (Other Startups’ results; no lift of your own yet) × 6 a month × $598 per Pro-to-Team upgrade ≈ +$646. Under First signal: At 6 a month, 90 days can tell only +36 per 100 or more, so your file starts at holdout: and reads before-after.
sample · api.sb/
Start it this week
npx startup-builder add play usage-upsell # 1 · writes plays/usage-upsell.mdx (Not live yet, gaps #5)
# 2 · dry run; below, its 422 on 2026-08-24
curl -X POST "https://api.sb/leaseline.app/campaigns?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"usage-upsell"}'
# 3 · keep it, at You approve each one
curl -X POST https://api.sb/leaseline.app/campaigns \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"usage-upsell"}'
# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/usage-upsell \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":null}'
# off: no ask sends; the bound answer keeps showing
curl -X PATCH https://api.sb/leaseline.app/campaigns/usage-upsell \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":"now"}' | Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | 41 Customers, 6 a month pacing past a Limit | POST /{namespace}/metrics/query |
limit-on-offer pass · Not live yet, gaps #8 | Leaseline Pro: 50 units | POST /{namespace}/offers/{offer}/publish |
billing-connected fail · Not live yet, gaps #8 | Connect Stripe so every charge and Meter reading is live. | POST /{namespace}/integrations/connect |
baseline warn · Not live yet, gaps #8 | n 9: it reads before-after, not proof, until volume allows a held-back half. | POST /{namespace}/metrics/query |
role-unstaffed warn · Not live yet, gaps #8 | Staff the CSM, or leave it: 4 Customers Priya owns get drafts in her queue. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "usage-upsell can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "41 Customers, 6 a month pacing past a Limit",
"doc": "/plays/usage-upsell#check-use-when",
"then": "Metric.query"
},
{
"check": "limit-on-offer",
"status": "pass",
"observed": "Leaseline Pro: 50 units",
"doc": "/plays/usage-upsell#check-limit-on-offer",
"then": "Offer.publish"
},
{
"check": "billing-connected",
"status": "fail",
"doc": "/plays/usage-upsell#check-billing-connected",
"fix": "Connect Stripe so every charge and Meter reading is live.",
"then": "Integration.connect"
},
{
"check": "baseline",
"status": "warn",
"observed": "1 upgrade of 9 pacing Customers in 90 days",
"doc": "/plays/usage-upsell#check-baseline",
"fix": "n 9: it reads before-after, not proof, until volume allows a held-back half.",
"then": "Metric.query"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"success-manager",
"billing-specialist"
],
"owned": 4,
"doc": "/plays/usage-upsell#check-role-unstaffed",
"fix": "Staff the CSM, or leave it: 4 Customers Priya owns get drafts in her queue.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
use-when · limit-on-offer · pass: 41 Customers, 6 a month pacing; Pro’s 50 units (gaps #8) | POST /{namespace}/metrics/query on limit-upgrade-arr; POST /{namespace}/offers/{offer}/publish adds a Limit | 1 min | none |
billing-connected · fail | POST /{namespace}/integrations/connect with system recurring-billing (Stripe) | 5 min | the first Meter sync, about 10 min |
baseline · warn: 1 of 9 in 90 days | POST /{namespace}/metrics/query; it fills Before in Your number | 0 min | none |
role-unstaffed · warn: 4 of your Customers have a person owner, Priya | POST /{namespace}/roles/{role}/staff, or leave it: CSM routes come to you, and Priya’s 4 get drafts in her queue | 0 min | none |
Your number · Before $598 · Now $2,
Each note costs $0.04 from your Domain (Worker cost), and Meter reads and POST /{namespace}/entitlements/check cost nothing. That comes to about $0.25 a month at 3 pacing Customers and $0.50 at 6.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A Customer’s use is on pace to pass a Limit before it resets | limit-pace | threshold on Entitlement.checked where used / | 3d; once per Entitlement per period | your app calls POST /{namespace}/entitlements/check, or POST /{namespace}/events/capture |
| An invoice bills overage past the step, two periods running | overage-past-step | threshold on Invoice.billed where overage > step, 2 periods running | 7d | POST /{namespace}/integrations/connect (Stripe) |
| An annual plan at 80 %+ of its Limit nears renewal | usage-renewal-window | at on Renewal.opened where 60 days before renewsAt, at 80 % or more of its Limit | 30d | POST /{namespace}/integrations/connect (Stripe) |
+ 6 more for limit approach and reach, the first month, asks for more, reversals and pricing visits (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1204" ,
"$type": "Event",
"name": "Entitlement.checked",
"at": "2026-10-19T09:41:12Z" ,
"seq": 1204,
"data": {
"customer": "customer_12",
"limit": "units",
"used": 118,
"bound": 50,
"atBound": "overage",
"resets": "2026-11-01"
}
}npx startup-builder trigger Entitlement.checked --like event_1204 # replays the delivery
npx startup-builder clock advance --by 3d # then runs the day-3 note step
npx startup-builder listen Entitlement.checked --forward-to http://localhost:3000/hook Who runs it
CSM · your agent (role:, worker_5) holds the Key Result. It hands to each Customer’s owner: AE · Priya Shah (person) for the 4 she owns and for every Quote over 150 units, and you (Dana Okafor, person) for the rest, because the CSM and Billing Specialist Roles have no person. Your number links to Did it work.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Offer the next plan · Show the saving | It does it, you can stop it | 6 of 6 · 3 of 3 | met on day 12 (the first 5 notes) | any note fails a Check, or the misquote Stop fires → You approve each one | POST /{namespace}/actions/{action}/cancel inside the hold |
| Offer it at renewal | You approve each one | 1 of 1 | at 10 passed | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| To the AE · Not now | It does it | 1 of 1 · 2 of 2 | — | a held Customer is asked anyway → You approve each one | POST /{namespace}/actions/{action}/redirect |
| Change the upgrade rule | You approve each one | 0 merged | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a discount, a deadline that isn’t real, a switch down hidden | To a person, every time | 1 asked, 1 handed over | never | — | POST /{namespace}/approvals/{approval}/approve on the AE’s Quote |
What it reads · What it may call. It reads the Meter, the Entitlement and the live Price, and never a pricing-Page visit or a health score in what it writes. POST /{namespace}/subscriptions/{subscription}/switch is the Customer’s own press, never the agent’s call. Its tools are POST /{namespace}/entitlements/check, POST /{namespace}/messages/{message}/schedule, POST /{namespace}/conversations/{conversation}/answer, POST /{namespace}/conversations/{conversation}/escalate, POST /{namespace}/quotes/{quote}/send, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.
You hold the CSM Key Result: $3,000 limit upgrade ARR by 2026-12-31. ← worker, metric
On each Trigger, choose one option and say why; a held Customer gets Not now. ← triggers, chooses, keep-first
Every number comes from the headroom card, with its as-of time. ← copyRules 1
Suggest the upgrade only when it costs less than the overage it replaces. ← copyRules 3, your file line 4
The switch down sits on the same billing Page, one press away. ← copyRules 8 The slow loop. The weekly Decisions kept the $50 step as the line, and no Change was merged in the window, so there is no replay or read-back to print.
What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Offer the next plan offer-next-plan | overage passes the step to the next Offer | Your rule (line 4) | usage-upgrade-note → on press change-plan → usage-upgrade-confirm | the Customer’s press | the same |
Show the saving show-the-saving | staying costs less than the next Offer | Your rule (line 4) | overage-savings-note | nobody | the same |
Offer it at renewal at-renewal | an annual term renews within 60 days | Agent’s call, with its reason | usage-upgrade-note at renewal | you approve it · Take this one | you |
To the AE to-the-ae | over 150 units, or asks for terms | Agent’s call, with its reason | POST /{namespace}/conversations/{conversation}/escalate → POST /{namespace}/quotes/{quote}/send | POST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 1 business day · Take this one | you |
Not now not-now | another Play holds them, or keep-first applies | Your rule (rule j) | nothing; names the holder | their CSM, so you · Take this one | you |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | “Move us to Team.” | the note’s one-press link; nothing is charged until they press | the Customer | — | POST /{namespace}/subscriptions/{subscription}/switch (theirs) |
| Send me pricing | “What would Team cost us?” | the headroom card again, with the live Price and today’s charge | CSM · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Not this month.” | the ask closes, and customer-ask-cap frees the Customer | nobody | at once | GET /{namespace}/decisions |
| Wrong person | “Billing is Lee’s call.” | asks them to forward it; it never writes to an address from their text | CSM · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply until Oct 28 | the note waits for the return date | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Can we keep Pro’s price on Team?” | To a person: a price is never the agent’s call | AE · Priya Shah, or you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “Stop emailing us about plans.” | notes stop; the in-product notice keeps running | nobody | at once | POST /{namespace}/contacts/{contact}/suppress |
When it hands to you
On this Play, hand-offs go to the Customer’s owner, the CSM’s seat. One Brief per Role, as captured (brief is Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_402" ,
"$type": "Task",
"assignee": "worker_3",
"due": "2026-10-08T11:40:00Z" ,
"on": "customer_7",
"brief": {
"who": "Birchway Properties, on the $2,400 plan" ,
"said": "We’re adding 2 owners: about 420 units by January." ,
"chose": "to-the-ae",
"why": "over 150 units; a Quote is the AE’s" ,
"never": [
"a discount",
"a deadline that isn’t real"
]
}
},
{
"$id": "https://api.sb/leaseline.app/tasks/task_388" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-03T17:00:00Z" ,
"on": "customer_19",
"brief": {
"who": "Harbor Row",
"chose": "not-now",
"why": "at risk since Oct 2: 3 support threads on late notices" ,
"held": "keep-first"
}
}
]The owner answers with POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, or POST /{namespace}/actions/{action}/redirect, each a one-call window. Once a person claims, the agent’s pending asks to that Customer cancel with claimedByPerson (Not live yet, gaps #7), and every reply goes to the owner. A Task nobody claims by its due time goes up a Role through POST /{namespace}/tasks/{task}/escalate, and person-promise fires.
On time: 3 of 3 claimed within their due time, median 40 min, n 3, ■□□□ Too early to tell beside Other Startups’ 94 % on time. Your queue: in week 1, 6 approvals and 1 hand-off; after the promotion, about 1 hand-off a week.
The fixed line: POST /{namespace}/deals/{deal}/assign sets owner to the receiving person, the agent is never a Deal’s owner, and Meetings land on the owner’s calendar under the owner’s name.
When it won’t send
| Check (refuses one send) | Refuses | Fix | Then |
|---|---|---|---|
owned-by-a-person (rule j, gaps #8) | a note to a Customer whose open Deal a person owns, or whose Conversation a person claimed | Leave it to the person who owns it: the draft waits in their queue. | POST /{namespace}/tasks/{task}/assign |
keep-first (rule j, gaps #8; the record’s At-risk hold) | an upgrade ask to a Customer who is at risk, past due or has an unsubscribe pending | Let their CSM reach them first, then ask again once they are healthy. | POST /{namespace}/tasks/next |
customer-ask-cap (gaps #3) | a second open ask to one Customer in a window, from any Play | annual-prepay holds them until Nov 3. | GET /{namespace}/decisions |
reconcile (the record’s Reconcile stop; Not live yet, gaps #8) | a Customer whose Subscription and Stripe disagree | Clear the Stripe mismatch first. | POST /{namespace}/tasks/next |
| Stop (pauses the Play) | Pauses when | Then |
|---|---|---|
misquote | a Price, charge, Limit or reset time differs from the live Price or the Entitlement check | You approve each one |
meter-gap | headroom differs from the Meter or the Stripe usage by over 1 % | every note waits for the Billing Specialist, so you |
downgrade | over 10 % of upgrades switch down or end within 90 days | the pace lane pauses; the upgrade map is flagged to you |
logo-churn | treated Customers’ 90-day logo churn passes the comparison’s by 2 points | the pace lane pauses until the next readout |
surprise-charge | 2 refund requests or disputes on upgrade charges in 30 days | POST /{namespace}/incidents/open |
hostile-reply | 3 hostile replies about upgrades in 30 days | note lanes pause |
complaint | complaints near 0.1 % of notes | notes pause on every Mailbox |
unsubscribe-floor | unsubscribes over 1 % of notes in 30 days | note lanes pause; the in-product notice keeps running |
holdout-integrity | a notice reaches a held-back Customer; over 5 % voids the cohort’s readout | POST /{namespace}/tasks/{task}/assign |
model-spend | month-to-date model spend reaches its Limit | every ask about more use goes to a person |
kill-switch | you press it | every ask stops; the bound answer keeps showing |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that sent is the Maple Court thread in Example.
| Check | Fix | Then |
|---|---|---|
keep-first fail · Not live yet, gaps #8 | Let their CSM reach them first, then ask again once they are healthy. | POST /{namespace}/tasks/next |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The note to Harbor Row did not send: they are at risk.",
"checks": [
{
"check": "keep-first",
"status": "fail",
"doc": "/plays/usage-upsell#check-keep-first",
"fix": "Let their CSM reach them first, then ask again once they are healthy.",
"then": "Task.next"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "meter-gap",
"observed": "3.4 %",
"bound": "1 %",
"window": "1 sync"
},
"doc": "/plays/usage-upsell#stop-meter-gap" ,
"task": "task_351"
}
]Did it work
At 6 pacing Customers a month, no held-back half reads within 90 days, so the basis is before-after (rule o): the columns read Now and Before, and nothing prints as Added. Each figure’s call is POST /{namespace}/metrics/query by node, and every node above the leaf is Not live yet (gaps #4).
$2,
- Chose
- offer-next-plan
- Why
- 118 units on Pro: $68 of overage this period is more than the $50 step to Team
- From
- offer-next-plan · show-the-saving · at-renewal · to-the-ae · not-now
- Expects
- Subscription.switched to Team within 30d
- Reopens when
- no Subscription.switched for customer_12 within 30d
- By
- worker_5
JSON
{
"$id": "decisions/decision_340",
"$type": "Decision",
"choice": "offer-next-plan",
"reason": "118 units on Pro: $68 of overage this period is more than the $50 step to Team",
"stakes": "low",
"options": [
"offer-next-plan",
"show-the-saving",
"at-renewal",
"to-the-ae",
"not-now"
],
"by": "worker_5",
"on": "customer_12",
"expected": "Subscription.switched to Team within 30d",
"reopensWhen": "no Subscription.switched for customer_12 within 30d"
}| Node | Now (56 days) | Before (the 90 days, per 56) | Added | Confidence | Basis |
|---|---|---|---|---|---|
limit-upgrade-arr | $2,392: 4 of 9 matured upgraded (44 per 100) · 2 still maturing · Booked $2,990: credited, not all caused by this Play | $598: 1 of 9 (11 per 100) | — · Worth about +$650 a month | — | before-after, not proof · Other Startups’ results +18 per 100, ■■■□ Likely, n 2,240 |
expansion-arr ↑ | $3,588 | $1,196 | — | — | before-after; includes 1 seat resize this Play never asked for |
net-new-arr · arr | carried from expansion-arr | carried | — | — | carried |
Before against after reads one window against the 90 days before it, scaled to 56 days; it is not proof, and Other Startups’ results print beside it.
How it moves it: each note names Team’s live Price, today’s prorated charge ($20.00 for Kestrel) and the overage it replaces.
Guardrails: before against after: logo-churn, 0 of 5 upgraded Customers gone against a trailing 2.1 % a month, and contraction-arr, $0 from switches down against $347 before; 3 upgrades are still inside their 90 days, still deciding.
{
"$id": "https://api.sb/leaseline.app/campaigns/usage-upsell" ,
"$type": "Campaign",
"play": "play:usage-upsell@1" ,
"path": [
"limit-upgrade-arr",
"expansion-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "limit-upgrade-arr",
"unit": "usd",
"heldBack": 0,
"decision": null
},
"readout": {
"value": null,
"n": 9,
"maturing": 2,
"interval": null,
"readability": "inadmissible",
"basis": "before-after",
"pooled": true
},
"ledger": {
"spend": 1,
"workerCost": 1,
"hours": 0.9,
"pipelineArr": 2400,
"wonArr": 2990,
"projectedArr": 646,
"paybackDays": 1
}
}Outside benchmark: 0.75 % of Subscriptions move to a higher Offer in their first month (10.5M purchases at 4,029 companies, a subscription-analytics firm’s billing data, 2026-09-30).
Before it · After it. Before it: instant-response and product-led feed it the trials that convert (Offer.subscribed). After it: annual-prepay hears steady-usage after 3 periods on Team, and customer-referrals hears the Customers it upgraded; keep-first holds both while a Customer is at risk.
Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days): 13 Decisions on 11 Customers, 9 of them matured past their 30-day window. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Decisions | Upgraded | Booked (credited, not all caused by this Play) |
|---|---|---|---|
| Offer the next plan | 6 | 5 to Team, 2 still maturing | $2,990 |
| Show the saving · Offer it at renewal · To the AE · Not now | 3 · 1 · 1 · 2 | 0 · 0 (still deciding) · 1 Quote open · 0 | $2,400 pipeline |
| At 3 a month (the Use it when volume) | about 6 in 56 days | before-after, not proof | Other Startups’ results: +18 per 100, ■■■□ Likely, n 2,240 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_119 Campaign.created | the three steps; dry run fails billing-connected | 12 min |
| 0 | event_120 Integration.connected | Stripe connected; the first Meter sync took 9 min | 5 min |
| 4 | event_207 Subscription.switched | the first upgrade paid: Kestrel, Pro to Team | — |
| 12 | event_388 Role.updated | promoted: notes send after a 2-minute hold | — |
| 17 | event_455 Campaign.paused | meter-gap, cleared in 3 h by you | 10 min |
| 39 | event_761 Action.refused | keep-first held Harbor Row; you took it with POST /{namespace}/tasks/{task}/claim | 15 min |
| 1–56 | event_142 Approval.created, first of 6 | you approved 6 notes; weekly reviews, from the POST /{namespace}/tasks/next log | 12 min · total 54 min measured, 1 h asked |
Leaseline → Kestrel’s billing owner, Oct 19, 09:44 · You’re at 118 units on Pro, 68 past its 50-unit Limit, so this period’s overage is $68.¹ Team is $79 a month for up to 150 units: $20.00 today, then $79 a month, and your overage stops.² Switch with one press; switching back sits on the same page.³ *Signed with the Role’s disclosure line.*⁴
Kestrel, Oct 19, 10:02 · pressed Switch to Team.
Subscription.switched, +$598 ARR.
- Upgrade this week and we’ll lock Team at $59 a month.
+ At 56 units, staying on Pro costs $6 in overage this period, less than the $50 step to Team, so staying is cheaper. Birchway Properties, 2026-10-08 10:40 · “We’re adding 2 owners: about 420 units by January.” · Leaseline, 10:42 · *Thanks. Priya, your account lead, will reply by 11:40 with a Quote.*⁵ The Brief went to Priya (
task_402).
¹ Every number comes from the headroom card, as of 09:41. ² The live Price and today’s prorated charge. ³ The switch down, one press away. ⁴ The fixed signature line, compiled from the Role’s disclosure Setting. ⁵ The routed line, compiled from the Role’s Settings.
Your settings
---
holdout: 0 # 6 a month is too few to hold half back; it reads before-after
---
Offer Team when overage passes the $50 step; under that, show them staying is cheaper. Over 150 units, bring in the AE. $ npx startup-builder show plays/usage-upsell
holdout 0 · step $50 · AE over 150 units your file plays/usage-upsell.mdx:2,4 (Ask 2026-08-24)
family expansion · worker role:success-manager, owns never · chooses 5 (3 rule, 2 read) · sequences usage-upgrade-note, overage-savings-note, usage-upgrade-confirm · checks owned-by-a-person, keep-first, customer-ask-cap, reconcile · stops 11 · placements gmail, outlook + in-product notice catalog play:usage-upsell@1 (Not live yet, gaps #5) Every key from the catalog
Compiled, not written: edit the Play record usage-upsell and its Leaseline run, then regenerate. Every step runs from a sentence or a line of code. In your words: the CRM words you know, and where each lives here.