Annual prepay
Offers a steady monthly Customer a year up front, at a discount their own churn pays for, and answers an annual cancel with one save.
- Step
- Keep Customers
- Channel
- Renewals
- Summary
- startupbuilder.dev
Be our CSM for renewals: when a Customer has paid monthly for 3 months and uses Leaseline steadily, offer them the year for $290, 2 months free, and show the dollars saved. Never offer it to anyone leaving. If someone cancels a year, offer the one save that fits, and never slow the cancel. 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":"annual-prepay"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'annual-prepay' }, { 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":"annual-prepay"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'annual-prepay' }, { 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": "annual-prepay can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "owned-by-a-person",
"status": "pass",
"observed": "4 of the 24 eligible Customers have a person owner: their drafts go to that person",
"doc": "/plays/ annual-prepay#check-owned-by-a-person",
"then": "Task.assign"
},
{
…
Hand our latest Customer at their third renewal 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(%22Subscription.renewed%22%2C%20%22Invoice.collected%22%2C%20%22Quote.requested%22%2C%20%22Renewal.opened%22%2C%20%22Subscription.unsubscribed%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_1418"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22customer_14%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_14%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Subscription.renewed", "Invoice.collected", "Quote.requested", "Renewal.opened", "Subscription.unsubscribed")', 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 (\"Subscription.renewed\", \"Invoice.collected\", \"Quote.requested\", \"Renewal.opened\", \"Subscription.unsubscribed\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1418"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"customer_14\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"customer_14\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Subscription.renewed\", \"Invoice.collected\", \"Quote.requested\", \"Renewal.opened\", \"Subscription.unsubscribed\")', 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_486",
"$type": "Decision",
"choice": "offer-the-year",
"reason": "Maple Court renewed Pro for the 4th time, used 31 to 34 units each month, and at your 3.8 % monthly churn the $58 discount pays for itself",
"stakes": "low",
"options": [
"offer-the-year",
"offer-on-the-quote",
"send-the-renewal-notice",
"keep-them",
"pause-it",
"switch-down",
…
sample · 4 Not live yetPreviewOn PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim
action_3 is the one offer note to Maple Court’s billing owner, held 2 minutes; the same offer shows on their billing page, and switching back to monthly sits beside it.
7 of 18 offered switched to the year · ■□□□ Too early to tell · n 0 matured · path → arr
| Try another: the Customer | Choice | Reason |
|---|---|---|
| Maple Court: 4th renewal on Pro, 31 to 34 units a month | offer-the-year | steady, past 3 renewals, and the discount pays for itself |
| Wrenfield Rentals: asked for a Quote to add 2 owners | offer-on-the-quote | a Quote is the AE’s; the year rides on it |
| Juniper Homes, on the year: turned off renewal, “we only lease in spring” | pause-it | a seasonal gap in their use |
| Harbor Row: at risk since Oct 2 | not-now | keep-first: their CSM sees them first |
| Oakmere Lettings, on the year: “we sold the portfolio” | let-them-go | they are leaving and no save fits; the cancel ran at once |
- 20 min week 1, then 5 a week
- Your time
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Prepay retention lift → Lost Customers ↓ → ARR added each week → ARR
Worth’s formula: about +2.1 points of starting ARR kept (Other Startups’ results; none of your terms have ended) × 12 eligible Customers a month × $348 starting ARR each ≈ +$88. Under First signal: The leaf reads 30 days after each annual term ends, so your first read lands in 2027-10; until then it prints Too early to tell beside Other Startups’ results, and the switch rate is what happened.
sample · api.sb/
Start it this week
npx startup-builder add play annual-prepay # 1 · writes plays/annual-prepay.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":"annual-prepay"}'
# 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":"annual-prepay"}'
# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/annual-prepay \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":null}'
# off: no offer sends; annual terms and renewal notices keep running
curl -X PATCH https://api.sb/leaseline.app/campaigns/annual-prepay \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":"now"}' | Check | Fix | Then |
|---|---|---|
owned-by-a-person pass · Not live yet, gaps #8 | 4 of the 24 eligible Customers have a person owner: their drafts go to that person | POST /{namespace}/tasks/{task}/assign |
keep-first warn · Not live yet, gaps #8 | Their CSM sees them first; they get no offer until health recovers. | POST /{namespace}/tasks/next |
customer-ask-cap pass · Not live yet, gaps #3 | 1 of the 24 has an open ask from usage-upsell, held until Nov 3 | GET /{namespace}/decisions |
use-when pass · Not live yet, gaps #8 | 24 Customers past their 3rd renewal; 3.8 % monthly churn among them | POST /{namespace}/metrics/query |
billing-connected fail · Not live yet, gaps #8 | Connect Stripe so every total and renewal date is live. | POST /{namespace}/integrations/connect |
baseline warn · Not live yet, gaps #8 | n 24: the year reads beside Other Startups’ results until terms end. | POST /{namespace}/metrics/query |
role-unstaffed warn · Not live yet, gaps #8 | Staff the CSM, or leave it: saves and calls come to you. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "annual-prepay can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "owned-by-a-person",
"status": "pass",
"observed": "4 of the 24 eligible Customers have a person owner: their drafts go to that person",
"doc": "/plays/annual-prepay#check-owned-by-a-person",
"then": "Task.assign"
},
{
"check": "keep-first",
"status": "warn",
"observed": "2 of the 24 are at risk",
"doc": "/plays/annual-prepay#check-keep-first",
"fix": "Their CSM sees them first; they get no offer until health recovers.",
"then": "Task.next"
},
{
"check": "customer-ask-cap",
"status": "pass",
"observed": "1 of the 24 has an open ask from usage-upsell, held until Nov 3",
"doc": "/plays/annual-prepay#check-customer-ask-cap",
"then": "Decision.list"
},
{
"check": "use-when",
"status": "pass",
"observed": "24 Customers past their 3rd renewal; 3.8 % monthly churn among them",
"doc": "/plays/annual-prepay#check-use-when",
"then": "Metric.query"
},
{
"check": "billing-connected",
"status": "fail",
"doc": "/plays/annual-prepay#check-billing-connected",
"fix": "Connect Stripe so every total and renewal date is live.",
"then": "Integration.connect"
},
{
"check": "baseline",
"status": "warn",
"observed": "0 annual terms ever; 11 of 24 monthly Customers lost in the past year",
"doc": "/plays/annual-prepay#check-baseline",
"fix": "n 24: the year reads beside Other Startups’ results until terms end.",
"then": "Metric.query"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"success-manager",
"billing-specialist"
],
"owned": 4,
"doc": "/plays/annual-prepay#check-role-unstaffed",
"fix": "Staff the CSM, or leave it: saves and calls come to you.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
owned-by-a-person · keep-first · customer-ask-cap · pass · warn: 2 at risk · pass: 1 held by usage-upsell (gaps #3, #8) | POST /{namespace}/tasks/next shows the 2 to their CSM; GET /{namespace}/decisions names the holder | 2 min | none |
use-when · billing-connected · pass: 24 eligible · fail | POST /{namespace}/integrations/connect with system recurring-billing (Stripe) | 5 min | the first sync, about 10 min |
baseline · warn: 0 annual terms, 11 of 24 lost in a year | 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: saves and calls come to you, and Priya’s 4 get drafts in her queue | 0 min | none |
Your number · Before 0 annual terms · Now 7 of 18 offered on the year · Target 10 by Dec 31 · ■□□□ Too early to tell · n 0 matured · held back at random
Each switch costs its discount, $58 a year on Pro, booked as contraction; each note costs $0.04 from your Domain (Worker cost), and the Quote and Stripe reads cost nothing. That comes to about $0.50 a month in notes and $230 a month in discounts at 4 switches.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A monthly Customer renews for the 3rd time or later | third-renewal | event on Subscription.renewed where a monthly term's 3rd renewal or later, and no annual term | the note within 1 d; ignored after 30 d | POST /{namespace}/integrations/connect (Stripe) emits Subscription.renewed |
| A Customer on the year turns off renewal | annual-term-cancelled | event on Subscription.unsubscribed where on an annual term, or a refund ask for the annual charge | the save offer beside the cancel, at once; 14 d | the Customer’s own POST /{namespace}/subscriptions/{subscription}/unsubscribe |
| 60 days before an annual term renews | annual-renewal-opened | at on Renewal.opened where 60 days before renewsAt, inside the renewal notice window | the notice, inside the window | POST /{namespace}/renewals/open |
+ 2 more for steady usage and Quote requests (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1418" ,
"$type": "Event",
"name": "Subscription.renewed",
"at": "2026-10-19T00:04:51Z" ,
"seq": 1418,
"data": {
"subscription": {
"$id": "https://api.sb/leaseline.app/subscriptions/subscription_14" ,
"customer": "customer_14",
"offer": "pro",
"interval": "month",
"renewals": 4,
"price": 29
},
"units": {
"min": 31,
"max": 34
}
}
}npx startup-builder trigger Subscription.renewed --like event_1418 # replays the 4th renewal
npx startup-builder clock advance --by 7d # then runs the offer’s one reminder on the billing page
npx startup-builder listen Subscription.renewed --forward-to http://localhost:3000/hook Who runs it
CSM · your agent (role:, worker_5) holds the Key Result. It hands Quotes to AE · Priya Shah (person) and the 4 Customers she owns, and save calls and every held Customer to you (Dana Okafor, person), 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 year up front · Send the renewal notice | It does it, you can stop it | 10 of 10 approved unedited · 0 notices due yet | met on day 26 (the first 10 notes) | a misquote Stop → You approve each one | POST /{namespace}/actions/{action}/cancel inside the hold |
| Offer it on the Quote | You approve each one | 2 of 2 | at 10 passed | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| Keep them · Pause it · Switch to a smaller plan · Book a call | You approve each one, on every rung: stakes high | 3 offered of 3 annual cancels; 1 kept, 1 paused | never | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| Let them go · Not now | It does it | 1 of 1 · 5 of 5 | — | a held Customer is offered anyway → You approve each one | POST /{namespace}/actions/{action}/redirect |
| Never: a discount over the cap, a deadline that isn’t real, a cancel that waits | To a person, every time | 1 asked for more, 1 handed over | never | — | POST /{namespace}/approvals/{approval}/approve on the AE’s Quote |
What it reads · What it may call. It reads the Customer’s own billing record: renewals, Price, units and spend, and states nothing else. POST /{namespace}/subscriptions/{subscription}/switch to the year is the Customer’s own press; POST /{namespace}/subscriptions/{subscription}/unsubscribe always runs at once. Its tools are POST /{namespace}/quotes/{quote}/send, POST /{namespace}/tasks/{task}/assign, POST /{namespace}/renewals/open, POST /{namespace}/subscriptions/{subscription}/keep, POST /{namespace}/subscriptions/{subscription}/pause, POST /{namespace}/subscriptions/{subscription}/switch, POST /{namespace}/meetings/book, and any other call becomes an Approval under the Role’s Mandate.
You hold the CSM Key Result: 10 Customers on the year by 2026-12-31, with none lost to the discount. ← worker, metric
On each 3rd renewal, Quote request or annual cancel, choose one option and say why. ← triggers, chooses
Show the annual total first, then the monthly equivalent and the dollars saved. ← copyRules 1
The discount never passes 17 %, and never goes to anyone leaving. ← your file, line 2; copyRules 9
A cancel runs at once; a save offer sits beside it, never before it. ← exit-never-waits The slow loop. The weekly Decisions kept the 3-renewal line and the 17 % cap, 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 year up front offer-the-year | past 3 renewals, steady, and the discount pays for itself | Your rule (line 5) | prepay-offer-note → on press POST /{namespace}/subscriptions/{subscription}/switch to the year, POST /{namespace}/quotes/{quote}/send for its total | the Customer’s press | the same |
Offer it on the Quote offer-on-the-quote | an upgrade, seat or new-line Quote | Agent’s call, with its reason | POST /{namespace}/quotes/{quote}/send with a monthly and an annual line → POST /{namespace}/tasks/{task}/assign | POST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 1 business day · Take this one | you |
Send the renewal notice send-the-renewal-notice | an annual term in its notice window | Your rule (the renewal notice window) | POST /{namespace}/renewals/open → prepay-renewal-notice | nobody | the same |
Keep them keep-them | a reason a fix you shipped answers | Agent’s call, with its reason | offers POST /{namespace}/subscriptions/{subscription}/keep beside the cancel, after your OK | the Customer’s press | you approve it |
Pause it pause-it | a seasonal gap | Agent’s call, with its reason | offers POST /{namespace}/subscriptions/{subscription}/pause beside the cancel, after your OK | the Customer’s press | you approve it |
Switch to a smaller plan switch-down | paying for more than they use | Agent’s call, with its reason | offers POST /{namespace}/subscriptions/{subscription}/switch to a smaller plan, after your OK | the Customer’s press | you approve it |
Book a call book-a-call | a reason only a person can answer | Agent’s call, with its reason | offers POST /{namespace}/meetings/book on your calendar beside the cancel | you, by the call · Take this one | you |
Let them go let-them-go | leaving, and no save fits | Agent’s call, with its reason | nothing; the cancel already ran, and the term ends on its date | nobody | the same |
Not now not-now | another Play holds them, or keep-first | 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 | “Yes, switch us to the year.” | sends the switch link; the year starts at the next renewal date | CSM · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Send me pricing | “What’s the annual price for Team?” | answers with the Pricing Page’s annual Price and the dollars saved | CSM · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Ask me after our busy season.” | nothing else sends; the offer stays on their billing page | nobody | at once | POST /{namespace}/messages/{message}/schedule |
| Wrong person | “Our bookkeeper pays the bill.” | 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 29 | the one reminder waits for the return date | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Can we pay the year in two halves?” | To the AE, the question in the Brief; terms are a person’s | AE · Priya Shah (person) | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “No more offers, please.” | no offer sends again; billing notices still do | nobody | at once | POST /{namespace}/contacts/{contact}/unsubscribe |
When it hands to you
On this Play, hand-offs go to the Customer’s owner: Quotes to AE · Priya Shah (person), save calls and held Customers to you. One Brief per Role, as captured, then the Approval (brief, draft, route and checks are Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_281" ,
"$type": "Task",
"assignee": "worker_3",
"due": "2026-10-10T17:00:00Z" ,
"on": "customer_22",
"brief": {
"who": "Wrenfield Rentals, on Team monthly" ,
"said": "Can we add 2 owners next month?" ,
"chose": "offer-on-the-quote",
"why": "a Quote is the AE’s",
"lines": [
"Team monthly, $79" ,
"Team annual, $790: 2 months free"
],
"never": [
"a discount over 17 %",
"terms the Offer lacks"
]
}
},
{
"$id": "https://api.sb/leaseline.app/tasks/task_286" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-14T15:00:00Z" ,
"on": "customer_9",
"brief": {
"who": "Juniper Homes, on the year" ,
"said": "We only lease in spring; turning off renewal." ,
"chose": "book-a-call",
"why": "their reason was a feature question",
"cancel": "ran at once on 2026-10-13; the term ends 2027-04-30"
}
},
{
"$id": "https://api.sb/leaseline.app/tasks/task_288" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-16T17: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"
}
},
{
"$id": "https://api.sb/leaseline.app/approvals/approval_77" ,
"$type": "Approval",
"verdict": null,
"deadline": "2026-10-20T17:00:00Z" ,
"draft": "Hi Ana, you’ve renewed Pro 4 times. The year is $290, which works out to $24.17 a month: $58 less than paying monthly…" ,
"route": "offer-the-year",
"checks": [
{
"check": "price",
"status": "pass"
},
{
"check": "keep-first",
"status": "pass"
}
]
}
]The owner answers with POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, POST /{namespace}/actions/{action}/redirect, POST /{namespace}/approvals/{approval}/approve or POST /{namespace}/approvals/{approval}/refuse, each a one-call window, and an Approval unanswered by its deadline expires and the offer never sends. Once a person claims, the agent’s pending asks to that Customer cancel with claimedByPerson (Not live yet, gaps #7). A cancel never waits on any of it.
On time: 4 of 4 claimed within their due time, median 1 h 10 min, n 4, ■□□□ Too early to tell beside Other Startups’ 94 % on time. Your queue: in weeks 1 to 4, 10 approvals; since, about 1 hand-off and 1 save approval 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; Not live yet, gaps #8) | an offer to a Customer whose owner is a person | Leave it to the person who owns it: the draft waits in their queue. | POST /{namespace}/tasks/{task}/assign |
keep-first (rule j; Not live yet, gaps #8) | an offer to a Customer who is at risk, past due or leaving | Let their CSM reach them first, then ask again once they are healthy. | POST /{namespace}/tasks/next |
customer-ask-cap (rule j; Not live yet, gaps #3) | a second open ask to one Customer in a window, from any Play | Wait for the open ask to close; usage-upsell holds them until Nov 3. | GET /{namespace}/decisions |
exit-never-waits (Not live yet, gaps #3) | a save step that runs before the cancel, hides it, or asks a question first | Run the cancel at once; show the save beside it. | POST /{namespace}/subscriptions/{subscription}/unsubscribe |
price (Not live yet, gaps #1) | an offer or Quote with a discount over the 17 % cap | Get the discount approved, or quote at the cap. | POST /{namespace}/approvals/{approval}/approve |
renewal-notice | an annual renewal charge with no notice sent inside its window | Send the notice; the charge waits for its window to pass. | POST /{namespace}/renewals/open |
reconcile (Not live yet, gaps #8) | any send to a Customer whose Subscription and Stripe disagree | Sync the Subscription with Stripe, then the send goes. | POST /{namespace}/integrations/{integration}/sync |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the note that sent instead is the Kestrel thread in Example.
| Check | Fix | Then |
|---|---|---|
exit-never-waits fail · Not live yet, gaps #3 | Run the cancel at once; show the save beside it. | POST /{namespace}/subscriptions/{subscription}/unsubscribe |
keep-first pass · Not live yet, gaps #8 | — | POST /{namespace}/tasks/next |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The save for Oakmere Lettings did not show: it asked a question before the cancel.",
"checks": [
{
"check": "exit-never-waits",
"status": "fail",
"doc": "/plays/annual-prepay#check-exit-never-waits",
"fix": "Run the cancel at once; show the save beside it.",
"then": "Subscription.unsubscribe"
},
{
"check": "keep-first",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "misquote",
"observed": "the note said $24 a month; the year is $24.17 a month" ,
"bound": "0 differences",
"window": "1 note"
},
"doc": "/plays/annual-prepay#stop-misquote" ,
"note": "caught at approval, never sent"
}
]Did it work
The 24 eligible Customers were split at random, 18 offered and 6 held back (rules m–o), but the leaf reads 30 days after each annual term ends, so no unit has matured: the first read lands in 2027-10. Each figure’s call is POST /{namespace}/metrics/query by node and by arm; every node above the leaf is Not live yet (gaps #4).
7 of 18 offered switched to the year · ■□□□ Too early to tell · n 0 matured · path → arr
- Chose
- offer-the-year
- Why
- Maple Court renewed Pro for the 4th time, used 31 to 34 units each month, and at your 3.8 % monthly churn the $58 discount pays for itself
- From
- offer-the-year · offer-on-the-quote · send-the-renewal-notice · keep-them · pause-it · switch-down · book-a-call · let-them-go · not-now
- Expects
- Subscription.switched to the annual Offer by the next renewal, 2026-11-19
- Reopens when
- no Subscription.switched for customer_14 by 2026-11-19
- By
- worker_5
JSON
{
"$id": "decisions/decision_486",
"$type": "Decision",
"choice": "offer-the-year",
"reason": "Maple Court renewed Pro for the 4th time, used 31 to 34 units each month, and at your 3.8 % monthly churn the $58 discount pays for itself",
"stakes": "low",
"options": [
"offer-the-year",
"offer-on-the-quote",
"send-the-renewal-notice",
"keep-them",
"pause-it",
"switch-down",
"book-a-call",
"let-them-go",
"not-now"
],
"by": "worker_5",
"on": "customer_14",
"expected": "Subscription.switched to the annual Offer by the next renewal, 2026-11-19",
"reopensWhen": "no Subscription.switched for customer_14 by 2026-11-19"
}| Node | Agent half | Your half | Added | Confidence | Basis |
|---|---|---|---|---|---|
prepay-retention-lift | 18 offered; 7 on the year; 0 terms ended | 6 held back; 0 terms ended | — (0 matured) · Worth about +$90 a month | ■□□□ Too early to tell | held back at random · n 0 matured · 24 still maturing · Other Startups’ results +2.1 points, ■■□□ Early sign, n 3,900 |
churned-arr ↓ | $0 lost of $6,264 starting ARR (0 of 18) | $347 lost of $2,088 (1 of 6) | −$347 fewer lost [−$1,050, +$360] · Not carrying up yet | ■□□□ Too early to tell (0 vs 1 Customer) | the same assignment, 56 days |
net-new-arr · arr | −$406 of discounts, booked as contraction | −$347 lost | −$59, carried: the discount moves contraction-arr too | ■□□□ Too early to tell | carried from churned-arr, with contraction-arr |
The leaf counts the share of eligible Customers’ starting ARR still earned 30 days after their annual terms end, offered half minus held back, discounts included. Until then the switch count is what happened, never a lift.
How it moves it: each offer shows the year’s total, $290, then $24.17 a month and $58 saved, from the Customer’s own renewals, where they saw only the $29 monthly Price.
Guardrails: offered against held back: contraction-arr, $406 of discounts against $0, and refunds, 0 of 7 switches.
{
"$id": "https://api.sb/leaseline.app/campaigns/annual-prepay" ,
"$type": "Campaign",
"play": "play:annual-prepay@1" ,
"path": [
"prepay-retention-lift",
"churned-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "prepay-retention-lift",
"unit": "percent",
"heldBack": 6,
"decision": null
},
"readout": {
"value": null,
"n": 0,
"maturing": 24,
"interval": null,
"readability": "inadmissible",
"basis": "held-back",
"pooled": false
},
"ledger": {
"spend": 406,
"workerCost": 1,
"hours": 1.2,
"wonArr": 0,
"projectedArr": 88,
"paybackDays": null
}
}Outside benchmark: annual plans keep about 1.3 to 1.5 times the share of Customers a year later that monthly plans keep (a subscription-analytics firm’s 2025 billing data).
Before it · After it. Before it: usage-upsell and next-outcome bring Customers to steady use. After it: customer-referrals and review-requests hear the Customers on the year; keep-first holds both while a Customer is at risk, and the save Decision answers every annual cancel.
Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days). 24 monthly Customers were past their 3rd renewal: 18 offered, 6 held back at random. 3 Customers already on the year turned off renewal, and each met the save Decision. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Decisions | Done | ARR (what happened) |
|---|---|---|---|
| Offer the year up front · Offer it on the Quote | 16 · 2 | 6 switched · 1 switched, 1 Quote open | 7 on the year; −$406 of discounts |
| Send the renewal notice | 0 | no term in its window yet | — |
| Keep them · Pause it · Book a call (of 3 annual cancels) | 1 · 1 · 1 | 1 kept · 1 paused to spring · the call held, cancel stands | $347 kept, $347 paused |
| Let them go · Not now | 1 · 5 | the cancel ran at once · held by keep-first 2, customer-ask-cap 1, a person owner 2 | — |
| At 10 eligible (the Use it when volume) | about 7 offered, 3 held back | held back at random; a year before the first read | Other Startups’ results: +2.1 points, ■■□□ Early sign, n 3,900 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_1114 Campaign.created | the three steps; the dry run fails billing-connected | 12 min |
| 0 | event_1115 Integration.connected | Stripe connected; renewals and Prices synced in 9 min | 5 min |
| 6 | event_1161 Subscription.switched | the first annual term paid: Kestrel Rentals, $290 | — |
| 18 | event_1214 Campaign.paused | misquote caught at approval: $24 against $24.17; you fixed the template | 6 min |
| 26 | event_1265 Role.updated | promoted: offer notes send after a 2-minute hold | — |
| 50 | event_1371 Action.refused | exit-never-waits refused a save that asked first; the cancel ran at once | — |
| 1–56 | event_1128 Approval.created, first of 13 | 10 notes and 3 saves you approved, from the POST /{namespace}/tasks/next log | 45 min · total 1.2 h measured, 1.5 h asked |
Leaseline → Kestrel’s billing owner, Aug 30, 09:10 · You’ve renewed Pro 3 times. The year is $290, paid at your next renewal on Sep 4: that’s $24.17 a month, $58 less than paying monthly.¹ It renews on Sep 4, 2027; you can turn renewal off any time from your billing page, and the first 30 days are refundable.² *Signed with the Role’s disclosure line.*³
Kestrel, Aug 30, 11:42 · pressed Switch to the year.
Subscription.switchedon Sep 4, $290 collected.
- Before you go, can you tell us why you’re cancelling? Choose a reason to continue.
+ Your renewal is off, and your term runs to 2027-03-31. If you want it back, one press turns renewal on again. Juniper Homes, 2026-10-13 14:20 · turned off renewal: “We only lease in spring.” The cancel ran at once; Book a call went beside it, and Dana took it (
task_286, due Oct 14, 15:00). On the call at 14:30 the next day they chose a pause to March instead, which Dana approved.⁴
¹ The annual total comes first, then the monthly equivalent and the dollars saved. ² The start, renewal and refund dates, and the one-step way to turn renewal off. ³ The fixed signature line, compiled from the Role’s disclosure Setting. ⁴ A save never runs before the cancel.
Your settings
---
discount: 0.17 # at most 2 months free
holdout: 0.25 # 1 in 4 eligible Customers is held back, chosen at random
---
Offer the year after the 3rd renewal, to Customers whose units stay steady.
Never offer it to anyone leaving; offer one save beside the cancel. $ npx startup-builder show plays/annual-prepay
discount 0.17 · holdout 0.25 · the 3-renewal line · the save rule your file plays/annual-prepay.mdx:2,3,5,6 (Ask 2026-08-24)
family retention · worker role:success-manager, owns never · chooses 9 (3 rule, 6 read; 4 at stakes high) · sequences prepay-offer-note, prepay-renewal-notice · approval every-send · checks owned-by-a-person, keep-first, customer-ask-cap, exit-never-waits, price, renewal-notice, reconcile · stops 9 catalog play:annual-prepay@1 (the save Decision, gaps #9) Every key from the catalog
Compiled, not written: edit the Play record annual-prepay 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.