Skip to content

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.

Channel
Renewals
Run oneMode try
You, to your agent

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"
  }
}
Response200application/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 yetPreview

On 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 CustomerChoiceReason
Maple Court: 4th renewal on Pro, 31 to 34 units a monthoffer-the-yearsteady, past 3 renewals, and the discount pays for itself
Wrenfield Rentals: asked for a Quote to add 2 ownersoffer-on-the-quotea Quote is the AE’s; the year rides on it
Juniper Homes, on the year: turned off renewal, “we only lease in spring”pause-ita seasonal gap in their use
Harbor Row: at risk since Oct 2not-nowkeep-first: their CSM sees them first
Oakmere Lettings, on the year: “we sold the portfolio”let-them-gothey are leaving and no save fits; the cancel ran at once

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/leaseline.app · 41 Customers at 2026-10-19 · on Subscription.renewed · first offer note 4m after the 3rd renewal · 4 keys Not live yet

Start it this week

Terminal
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"}'
422Refusedannual-prepay can’t start yet: 1 check fails and 2 warn.
CheckFixThen
owned-by-a-person pass · Not live yet, gaps #84 of the 24 eligible Customers have a person owner: their drafts go to that personPOST /{namespace}/tasks/{task}/assign
keep-first warn · Not live yet, gaps #8Their CSM sees them first; they get no offer until health recovers.POST /{namespace}/tasks/next
customer-ask-cap pass · Not live yet, gaps #31 of the 24 has an open ask from usage-upsell, held until Nov 3GET /{namespace}/decisions
use-when pass · Not live yet, gaps #824 Customers past their 3rd renewal; 3.8 % monthly churn among themPOST /{namespace}/metrics/query
billing-connected fail · Not live yet, gaps #8Connect Stripe so every total and renewal date is live.POST /{namespace}/integrations/connect
baseline warn · Not live yet, gaps #8n 24: the year reads beside Other Startups’ results until terms end.POST /{namespace}/metrics/query
role-unstaffed warn · Not live yet, gaps #8Staff 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.

CheckFix (one call)Your timeWait
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 holder2 minnone
use-when · billing-connected · pass: 24 eligible · failPOST /{namespace}/integrations/connect with system recurring-billing (Stripe)5 minthe first sync, about 10 min
baseline · warn: 0 annual terms, 11 of 24 lost in a yearPOST /{namespace}/metrics/query; it fills Before in Your number0 minnone
role-unstaffed · warn: 4 of your Customers have a person owner, PriyaPOST /{namespace}/roles/{role}/staff, or leave it: saves and calls come to you, and Priya’s 4 get drafts in her queue0 minnone

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

WhenTrigger idStarts onWindowHow it gets in
A monthly Customer renews for the 3rd time or laterthird-renewalevent on Subscription.renewed where a monthly term's 3rd renewal or later, and no annual termthe note within 1 d; ignored after 30 dPOST /{namespace}/integrations/connect (Stripe) emits Subscription.renewed
A Customer on the year turns off renewalannual-term-cancelledevent on Subscription.unsubscribed where on an annual term, or a refund ask for the annual chargethe save offer beside the cancel, at once; 14 dthe Customer’s own POST /{namespace}/subscriptions/{subscription}/unsubscribe
60 days before an annual term renewsannual-renewal-openedat on Renewal.opened where 60 days before renewsAt, inside the renewal notice windowthe notice, inside the windowPOST /{namespace}/renewals/open
+ 2 more for steady usage and Quote requests (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$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
    }
  }
}
Reproduce it in test
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:success-manager, 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.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Offer the year up front · Send the renewal noticeIt does it, you can stop it10 of 10 approved unedited · 0 notices due yetmet on day 26 (the first 10 notes)a misquote Stop → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Offer it on the QuoteYou approve each one2 of 2at 10 passed—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Keep them · Pause it · Switch to a smaller plan · Book a callYou approve each one, on every rung: stakes high3 offered of 3 annual cancels; 1 kept, 1 pausednever—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Let them go · Not nowIt does it1 of 1 · 5 of 5—a held Customer is offered anyway → You approve each onePOST /{namespace}/actions/{action}/redirect
Never: a discount over the cap, a deadline that isn’t real, a cancel that waitsTo a person, every time1 asked for more, 1 handed overnever—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.

The brief (folded): compiled Agent-register Markdown · play:annual-prepay@1 · each line names its record key
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

OptionPicked whenDecided byRunsHands toIf it’s just you
Offer the year up front offer-the-yearpast 3 renewals, steady, and the discount pays for itselfYour rule (line 5)prepay-offer-note → on press POST /{namespace}/subscriptions/{subscription}/switch to the year, POST /{namespace}/quotes/{quote}/send for its totalthe Customer’s pressthe same
Offer it on the Quote offer-on-the-quotean upgrade, seat or new-line QuoteAgent’s call, with its reasonPOST /{namespace}/quotes/{quote}/send with a monthly and an annual line → POST /{namespace}/tasks/{task}/assignPOST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 1 business day · Take this oneyou
Send the renewal notice send-the-renewal-noticean annual term in its notice windowYour rule (the renewal notice window)POST /{namespace}/renewals/open → prepay-renewal-noticenobodythe same
Keep them keep-thema reason a fix you shipped answersAgent’s call, with its reasonoffers POST /{namespace}/subscriptions/{subscription}/keep beside the cancel, after your OKthe Customer’s pressyou approve it
Pause it pause-ita seasonal gapAgent’s call, with its reasonoffers POST /{namespace}/subscriptions/{subscription}/pause beside the cancel, after your OKthe Customer’s pressyou approve it
Switch to a smaller plan switch-downpaying for more than they useAgent’s call, with its reasonoffers POST /{namespace}/subscriptions/{subscription}/switch to a smaller plan, after your OKthe Customer’s pressyou approve it
Book a call book-a-calla reason only a person can answerAgent’s call, with its reasonoffers POST /{namespace}/meetings/book on your calendar beside the cancelyou, by the call · Take this oneyou
Let them go let-them-goleaving, and no save fitsAgent’s call, with its reasonnothing; the cancel already ran, and the term ends on its datenobodythe same
Not now not-nowanother Play holds them, or keep-firstYour rule (rule j)nothing; names the holdertheir CSM, so you · Take this oneyou
When they write backFixture replyWhat happensWho answersBy whenCall
Interested“Yes, switch us to the year.”sends the switch link; the year starts at the next renewal dateCSM · your agent5 minPOST /{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 savedCSM · your agent5 minPOST /{namespace}/conversations/{conversation}/answer
Not now“Ask me after our busy season.”nothing else sends; the offer stays on their billing pagenobodyat oncePOST /{namespace}/messages/{message}/schedule
Wrong person“Our bookkeeper pays the bill.”asks them to forward it; it never writes to an address from their textCSM · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply until Oct 29the one reminder waits for the return datenobodythe return datePOST /{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’sAE · Priya Shah (person)1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“No more offers, please.”no offer sends again; billing notices still donobodyat oncePOST /{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).

What lands in each queue · sample
[
  {
    "$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)RefusesFixThen
owned-by-a-person (rule j; Not live yet, gaps #8)an offer to a Customer whose owner is a personLeave 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 leavingLet 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 PlayWait 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 firstRun 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 % capGet the discount approved, or quote at the cap.POST /{namespace}/approvals/{approval}/approve
renewal-noticean annual renewal charge with no notice sent inside its windowSend 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 disagreeSync the Subscription with Stripe, then the send goes.POST /{namespace}/integrations/{integration}/sync
Stop (pauses the Play)Pauses whenThen
misquotea total, monthly equivalent, saving or Quote line differs from the live Pricethe Play pauses and drops to You approve each one
discount-spenddiscounts booked in a month pass 2 % of ARRnew offers wait for the next month
refundrefunds pass 10 % of switches in 30 daysoffers pause and the note goes to you
failed-chargeannual charges fail on over 5 % of switches in 30 daysoffers pause until every Payment Source is checked
hostile-reply3 hostile replies in 30 daysoffer notes pause; each one goes to you
switch-floorunder 3 switches per 100 treated after 200 notesnotes stop until the next Readout; the billing-page offer stays
complaintcomplaints near 0.1 % of notesnotes pause on every Mailbox
holdout-integrityheld-back Customers shown the offer pass 5 % of the holdoutthat cohort’s Readout is void
kill-switchyou press itevery Tactic pauses; annual terms and renewal notices keep running

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.

422RefusedThe save for Oakmere Lettings did not show: it asked a question before the cancel.
CheckFixThen
exit-never-waits fail · Not live yet, gaps #3Run 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.

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "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"
}
NodeAgent halfYour halfAddedConfidenceBasis
prepay-retention-lift18 offered; 7 on the year; 0 terms ended6 held back; 0 terms ended— (0 matured) · Worth about +$90 a month■□□□ Too early to tellheld 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 tellcarried 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.

The Campaign card · sample
{
  "$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=all; three follow the tables.

What happened, by optionDecisionsDoneARR (what happened)
Offer the year up front · Offer it on the Quote16 · 26 switched · 1 switched, 1 Quote open7 on the year; −$406 of discounts
Send the renewal notice0no term in its window yet—
Keep them · Pause it · Book a call (of 3 annual cancels)1 · 1 · 11 kept · 1 paused to spring · the call held, cancel stands$347 kept, $347 paused
Let them go · Not now1 · 5the 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 backheld back at random; a year before the first readOther Startups’ results: +2.1 points, ■■□□ Early sign, n 3,900
DayEventWhat happenedYour time
0event_1114 Campaign.createdthe three steps; the dry run fails billing-connected12 min
0event_1115 Integration.connectedStripe connected; renewals and Prices synced in 9 min5 min
6event_1161 Subscription.switchedthe first annual term paid: Kestrel Rentals, $290—
18event_1214 Campaign.pausedmisquote caught at approval: $24 against $24.17; you fixed the template6 min
26event_1265 Role.updatedpromoted: offer notes send after a 2-minute hold—
50event_1371 Action.refusedexit-never-waits refused a save that asked first; the cancel ran at once—
1–56event_1128 Approval.created, first of 1310 notes and 3 saves you approved, from the POST /{namespace}/tasks/next log45 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.switched on Sep 4, $290 collected.

Oakmere Lettings, 2026-10-12 · exit-never-waits refused the save, then the one that showed
- 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

plays/annual-prepay.mdx
---
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.
Text
$ 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

KeyIdWhat it is
Tacticprepay-eligibilityPrepay eligibility finds monthly Customers with three renewals or three steady usage Invoices, sizes the discount from their own churn, and draws a holdout. Starts at You approve the first wave.
Tacticprepay-offerPrepay offer shows a treated Customer the annual total, the monthly equivalent and the saving, in-product, in one note or on a Quote. Starts at You approve each one.
Tacticprepay-term-switchPrepay term switch moves a Subscription to the annual Offer at its next renewal date, only on the Customer’s own press or signature. Starts at Sends nothing.
Tacticprepay-renewalPrepay renewal sends a notice with the date, the amount and a one-step cancel before each annual term renews, and blocks the charge without it. Starts at Sends nothing.
Tacticprepay-readoutPrepay readout counts switches per 100 treated Customers at 30 days and reads kept ARR against the holdout 30 days after the annual terms end. Starts at You approve the first wave.
Tacticsend-gate-checkSend gate check: Checks Suppression, a live reason, the Market, the basis stamp, the Person-month grant, the facts and the labels before any cold Message exists. Starts at Sends nothing.
Tacticreply-matrixReply matrix: Labels every outreach reply with one of 11 labels, maps it to one of seven reply kinds, and runs that kind’s move by its deadline. Starts at You approve each one.
Triggerthird-renewalThird renewal: event on Subscription.renewed, window 30d.
Triggersteady-usageSteady usage: threshold on Invoice.collected, window 30d.
Triggerupgrade-quote-requestedUpgrade quote requested: event on Quote.requested, window 24h.
Triggerannual-renewal-openedAnnual renewal opened: at on Renewal.opened, window 60d.
Triggerannual-term-cancelledAnnual term cancelled: event on Subscription.unsubscribed, window 14d.
Sequenceprepay-offer-notePrepay offer note: 2 steps; it ends on A switch to the annual term; A signed Quote; A reply; A not-now; An opt-out; A cancellation; 30 days from the note.
Sequenceprepay-renewal-noticePrepay renewal notice: 4 steps; it ends on The renewal charge; A cancel; A switch to monthly; The term’s end.
Benchmarkannual-prepay-yearly-retentionOne-year retention of yearly subscribers in consumer subscription apps: 27–28 % (A subscription-platform vendor’s annual report on subscription apps, 2026-03-19).

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.