URL: /plays/annual-prepay · Id: `play:annual-prepay`

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

[Offers](https://startupbuilder.dev/business/nouns/offer) a steady monthly [Customer](https://startupbuilder.dev/business/nouns/customer) a year up front, at a discount their own churn pays for, and answers an annual cancel with one save.

```text title="Set it up · Ask"
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.
```

```text title="Run one · Ask"
Hand our latest Customer at their third renewal to the CSM agent and show me what it chose and why. Don’t send.
```

```ts title="Run one · SDK"
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 }
```

```json title="Run one · Response · sample"
{
  "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",
      "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"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_5",
      "verb": "Subscription.get",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_5",
      "verb": "Quote.create",
      "checks": [
        {
          "check": "price",
          "status": "pass"
        },
        {
          "check": "reconcile",
          "status": "pass"
        }
      ]
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_3",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingTime",
      "mode": "try",
      "by": "worker_5",
      "verb": "Message.schedule",
      "checks": [
        {
          "check": "owned-by-a-person",
          "status": "pass"
        },
        {
          "check": "keep-first",
          "status": "pass"
        },
        {
          "check": "customer-ask-cap",
          "status": "pass"
        },
        {
          "check": "price",
          "status": "pass"
        }
      ]
    }
  ]
}
```

`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 |

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$90 ARR a month at 12 eligible | 20 min week 1, then 5 a week | $58 discount per switch, $0.04 per note | Day 6: first annual term paid (`Subscription.switched`) | 10+ monthly Customers past 3 renewals, 2 %+ monthly churn | Under 10: `usage-upsell` · no annual Offer: publish one (<Op verb="Offer.publish" />) |

**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
```bash
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"}'
```


```json title="The dry run’s 422 · 2026-08-24 · sample"
{
  "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"
    }
  ]
}
```

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-owned-by-a-person"></a>`owned-by-a-person` · <a id="check-keep-first"></a>`keep-first` · <a id="check-customer-ask-cap"></a>`customer-ask-cap` · pass · warn: 2 at risk · pass: 1 held by `usage-upsell` (gaps #3, #8) | <Op verb="Task.next" /> shows the 2 to their CSM; <Op verb="Decision.list" /> names the holder | 2 min | none |
| <a id="check-use-when"></a>`use-when` · <a id="check-billing-connected"></a>`billing-connected` · pass: 24 eligible · fail | <Op verb="Integration.connect" /> with `system` recurring-billing (Stripe) | 5 min | the first sync, about 10 min |
| <a id="check-baseline"></a>`baseline` · warn: 0 annual terms, 11 of 24 lost in a year | <Op verb="Metric.query" />; it fills Before in Your number | 0 min | none |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn: 4 of your Customers have a person owner, Priya | <Op verb="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 | <Op verb="Integration.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 <Op verb="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 | <Op verb="Renewal.open" /> |
| + 2 more for steady usage and Quote requests (`?lanes=all`) | | | | |

```json title="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
    }
  }
}
```

```bash title="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.

| 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 | <Op verb="Action.cancel" /> inside the hold |
| Offer it on the Quote | You approve each one | 2 of 2 | at 10 passed | — | <Op verb="Approval.approve" /> · <Op verb="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 | — | <Op verb="Approval.approve" /> · <Op verb="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 | <Op verb="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 | — | <Op verb="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. <Op verb="Subscription.switch" /> to the year is the Customer’s own press; <Op verb="Subscription.unsubscribe" /> always runs at once. Its tools are <Op verb="Quote.send" />, <Op verb="Task.assign" />, <Op verb="Renewal.open" />, <Op verb="Subscription.keep" />, <Op verb="Subscription.pause" />, <Op verb="Subscription.switch" />, <Op verb="Meeting.book" />, and any other call becomes an Approval under the Role’s Mandate.

```md title="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
| 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 <Op verb="Subscription.switch" /> to the year, <Op verb="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 | <Op verb="Quote.send" /> with a monthly and an annual line → <Op verb="Task.assign" /> | <Op verb="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) | <Op verb="Renewal.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 <Op verb="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 <Op verb="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 <Op verb="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 <Op verb="Meeting.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 | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Not now | “Ask me after our busy season.” | nothing else sends; the offer stays on their billing page | nobody | at once | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply until Oct 29 | the one reminder waits for the return date | nobody | the return date | <Op verb="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 | <Op verb="Conversation.escalate" /> |
| Stop | “No more offers, please.” | no offer sends again; billing notices still do | nobody | at once | <Op verb="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).

```json title="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 <Op verb="Task.claim" /> (Take this one), <Op verb="Task.return" /> with a reason, <Op verb="Action.redirect" />, <Op verb="Approval.approve" /> or <Op verb="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:** <Op verb="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. | <Op verb="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. | <Op verb="Task.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. | <Op verb="Decision.list" /> |
| <a id="check-exit-never-waits"></a>`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. | <Op verb="Subscription.unsubscribe" /> |
| <a id="check-price"></a>`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. | <Op verb="Approval.approve" /> |
| <a id="check-renewal-notice"></a>`renewal-notice` | an annual renewal charge with no notice sent inside its window | Send the notice; the charge waits for its window to pass. | <Op verb="Renewal.open" /> |
| <a id="check-reconcile"></a>`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. | <Op verb="Integration.sync" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-misquote"></a>`misquote` | a total, monthly equivalent, saving or Quote line differs from the live Price | the Play pauses and drops to You approve each one |
| <a id="stop-discount-spend"></a>`discount-spend` | discounts booked in a month pass 2 % of ARR | new offers wait for the next month |
| <a id="stop-refund"></a>`refund` | refunds pass 10 % of switches in 30 days | offers pause and the note goes to you |
| <a id="stop-failed-charge"></a>`failed-charge` | annual charges fail on over 5 % of switches in 30 days | offers pause until every Payment Source is checked |
| <a id="stop-hostile-reply"></a>`hostile-reply` | 3 hostile replies in 30 days | offer notes pause; each one goes to you |
| <a id="stop-switch-floor"></a>`switch-floor` | under 3 switches per 100 treated after 200 notes | notes stop until the next Readout; the billing-page offer stays |
| <a id="stop-complaint"></a>`complaint` | complaints near 0.1 % of notes | notes pause on every Mailbox |
| <a id="stop-holdout-integrity"></a>`holdout-integrity` | held-back Customers shown the offer pass 5 % of the holdout | that cohort’s Readout is void |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | every 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.

```json title="The captured refusal · sample"
{
  "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"
    }
  ]
}
```


```json title="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 <Op verb="Metric.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`

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

```json title="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 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 <Op verb="Task.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.switched` on Sep 4, $290 collected.

```diff title="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
```mdx title="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
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-prepay-eligibility"></a>`prepay-eligibility` | Prepay 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. |
| Tactic | <a id="tactic-prepay-offer"></a>`prepay-offer` | Prepay 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. |
| Tactic | <a id="tactic-prepay-term-switch"></a>`prepay-term-switch` | Prepay 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. |
| Tactic | <a id="tactic-prepay-renewal"></a>`prepay-renewal` | Prepay 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. |
| Tactic | <a id="tactic-prepay-readout"></a>`prepay-readout` | Prepay 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. |
| Tactic | <a id="tactic-send-gate-check"></a>`send-gate-check` | Send 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. |
| Tactic | <a id="tactic-reply-matrix"></a>`reply-matrix` | Reply 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. |
| Trigger | <a id="trigger-third-renewal"></a>`third-renewal` | Third renewal: `event` on `Subscription.renewed`, window 30d. |
| Trigger | <a id="trigger-steady-usage"></a>`steady-usage` | Steady usage: `threshold` on `Invoice.collected`, window 30d. |
| Trigger | <a id="trigger-upgrade-quote-requested"></a>`upgrade-quote-requested` | Upgrade quote requested: `event` on `Quote.requested`, window 24h. |
| Trigger | <a id="trigger-annual-renewal-opened"></a>`annual-renewal-opened` | Annual renewal opened: `at` on `Renewal.opened`, window 60d. |
| Trigger | <a id="trigger-annual-term-cancelled"></a>`annual-term-cancelled` | Annual term cancelled: `event` on `Subscription.unsubscribed`, window 14d. |
| Sequence | <a id="sequence-prepay-offer-note"></a>`prepay-offer-note` | Prepay 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. |
| Sequence | <a id="sequence-prepay-renewal-notice"></a>`prepay-renewal-notice` | Prepay renewal notice: 4 steps; it ends on The renewal charge; A cancel; A switch to monthly; The term’s end. |
| Benchmark | <a id="benchmark-annual-prepay-yearly-retention"></a>`annual-prepay-yearly-retention` | One-year retention of yearly subscribers in consumer subscription apps: 27–28 % (A subscription-platform vendor’s annual report on subscription apps, 2026-03-19). |