Customer referrals
Asks Customers to invite a peer right after they get the result they came for, and rewards both once the peer’s first payment clears refunds.
- Step
- Earn referrals
- Channel
- Reviews and referrals
- Kind
- Word of mouth
- Summary
- startupbuilder.dev
Be our CSM: the day a Customer’s first renewal notice goes out 60 days early, ask them once to invite a peer. Both get half off a month, once the peer’s first payment clears the 30-day refund window. Never write their words for them. 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":"customer-referrals"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'customer-referrals' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-07-21 · Not live yet, gaps #2
npx startup-builder do Campaign.create --json '{"play":"customer-referrals"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'customer-referrals' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-07-21 · Not live yet, gaps #2",
"mode": "try"
}
}application/json{
"type": "https: //startupbuilder.dev/ errors/ refused",
"title": "Refused",
"status": 422,
"detail": "customer-referrals can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "31 Customers reached the first notice sent 60 days out",
"doc": "/plays/ customer-referrals#check-use-when",
"then": "Metric.query"
},
{
…
Hand our latest Customer who reached the outcome 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(%22Outcome.delivered%22%2C%20%22Visitor.identified%22%2C%20%22Order.paid%22%2C%20%22Monitor.checked%22%2C%20%22Order.refunded%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_1301"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22customer_21%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_21%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Outcome.delivered", "Visitor.identified", "Order.paid", "Monitor.checked", "Order.refunded")', 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 (\"Outcome.delivered\", \"Visitor.identified\", \"Order.paid\", \"Monitor.checked\", \"Order.refunded\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1301"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"customer_21\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"customer_21\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Outcome.delivered\", \"Visitor.identified\", \"Order.paid\", \"Monitor.checked\", \"Order.refunded\")', 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_684",
"$type": "Decision",
"choice": "ask-after-outcome",
"reason": "Alder Street Rentals sent their first renewal notice 60 days out at 08: 10; they sit in the asked 90 %, and no other Play holds them",
"stakes": "low",
"options": [
"ask-after-outcome",
"send-their-invite",
"release-the-reward",
"hold-the-reward",
"offer-partner-terms",
"not-now"
…
sample · 4 Not live yetPreviewOn PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim
action_1 is Alder Street’s own code. action_2 is the ask in their existing thread, held to 09:00 their time; one reminder follows in 7 days only if no one has signed up.
9.8 paid referrals per 100 Customers against 2.7 before · before-after,
| Try another: the Customer | Choice | Reason |
|---|---|---|
| Alder Street Rentals: first renewal notice sent 60 days out, today | ask-after-outcome | the declared outcome; no other Play holds them |
| The invitee from Juniper Homes’ code paid with Juniper’s card | hold-the-reward | a card match: you decide before anything releases |
| Kestrel Rentals’ invitee: first payment past its 30-day window | release-the-reward | cleared, no match: both credits release |
| Harbor Row: at risk since Oct 2 | not-now | keep-first: their CSM sees them first |
- 20 min week 1, then 10 min a week
- Your time
- Day 9: the first referred Customer pays
- First signal
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Paid referrals → Referral share → New paying Customers → New Customers ↑ → ARR added each week → ARR
Worth’s formula: about +3.1 paid referrals per 100 asked (Other Startups’ results; no lift of your own yet) × 10 asked a month × $348 first order ARR ≈ +$108. Under First signal: At 10 asks a month, the 10 % held back reads nothing within a year, so this page reads before-after and pools with Other Startups.
sample · api.sb/
Start it this week
npx startup-builder add play customer-referrals # 1 · writes plays/customer-referrals.mdx (Not live yet, gaps #5)
# 2 · dry run; below, its 422 on 2026-07-21
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":"customer-referrals"}'
# 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":"customer-referrals"}'
# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/customer-referrals \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":null}'
# off: no ask sends; codes keep working and earned credits release
curl -X PATCH https://api.sb/leaseline.app/campaigns/customer-referrals \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":"now"}' | Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | 31 Customers reached the first notice sent 60 days out | POST /{namespace}/metrics/query |
billing-connected fail · Not live yet, gaps #8 | Connect Stripe so no credit releases on a refunded or disputed payment. | POST /{namespace}/integrations/connect |
baseline warn · Not live yet, gaps #8 | n 1: it reads before-after, not proof; the 10 % held back pools with Other Startups. | POST /{namespace}/metrics/query |
role-unstaffed warn · Not live yet, gaps #8 | Leave them: held rewards come to you; Priya’s 4 get drafts in her queue. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "customer-referrals can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "31 Customers reached the first notice sent 60 days out",
"doc": "/plays/customer-referrals#check-use-when",
"then": "Metric.query"
},
{
"check": "billing-connected",
"status": "fail",
"doc": "/plays/customer-referrals#check-billing-connected",
"fix": "Connect Stripe so no credit releases on a refunded or disputed payment.",
"then": "Integration.connect"
},
{
"check": "baseline",
"status": "warn",
"observed": "1 paid referral in 90 days, from the code on the settings page",
"doc": "/plays/customer-referrals#check-baseline",
"fix": "n 1: it reads before-after, not proof; the 10 % held back pools with Other Startups.",
"then": "Metric.query"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"risk-analyst",
"partner-manager"
],
"owned": 4,
"doc": "/plays/customer-referrals#check-role-unstaffed",
"fix": "Leave them: held rewards come to you; Priya’s 4 get drafts in her queue.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
use-when · pass: 31 Customers at the outcome (gaps #8) | POST /{namespace}/metrics/query on paid-referrals | 1 min | none |
billing-connected · fail | POST /{namespace}/integrations/connect with system recurring-billing (Stripe) | 5 min | the first sync, about 10 min |
baseline · warn: 1 in 90 days | POST /{namespace}/metrics/query; it fills Before in Your number | 0 min | none |
role-unstaffed · warn (gaps #8): 4 of your Customers have a person owner, Priya | POST /{namespace}/roles/{role}/staff, or leave it: held rewards come to you | 0 min | none |
Your number · Before 2.7 per 100 Customers · Now 9.8 paid referrals per 100 Customers · Target 12 by Dec 31 · before-after,
Each paid referral costs $29 in credit, $14.50 to each side, released after the 30-day refund window; the month’s credits are capped at 10 % of last month’s collected revenue. Asks cost $0.04 each (Worker cost). That comes to about $40 a month.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A Customer reaches the outcome you declared | delivered-outcome | event on Outcome.delivered where the Offer’s declared delivered-outcome Event, once per Customer | 7d | your app records the outcome (POST /{namespace}/events/capture) |
| A referred Customer’s first Order is paid | referral-first-payment | event on Order.paid where a referred Customer’s first Order | 30d | POST /{namespace}/integrations/connect (Stripe) |
| That payment’s refund window closes clean | refund-window-closed | at on Monitor.checked where the end of the refund window on a referred first Order, with no refund or Dispute | 7d | the daily release run |
+ 5 more for signups on a code, reversals, bursts, graduations and public pages carrying a code (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1301" ,
"$type": "Event",
"name": "Outcome.delivered",
"at": "2026-10-19T13:10:08Z" ,
"seq": 1301,
"data": {
"customer": "customer_21",
"name": "Alder Street Rentals",
"outcome": "first renewal notice sent 60 days out",
"offer": "leaseline-pro",
"heldBack": false
}
}npx startup-builder trigger Outcome.delivered --like event_1301 # replays the delivery
npx startup-builder clock advance --by 7d # runs the one reminder if no signup
npx startup-builder listen Outcome.delivered --forward-to http://localhost:3000/hook Who runs it
CSM · your agent (role:, worker_5) holds the Key Result. It hands held rewards to you (Dana Okafor, person, worker_1), because the Risk Analyst Role has no person, and a referrer at 5 paid referrals to Partnerships · your agent (worker_10) for Partner terms. A Customer AE · Priya Shah (person) owns gets drafts in her queue. Your number links to Did it work.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Ask after the outcome · Send their typed invite | It does it, you can stop it | 30 of 30 · 6 of 6 | met on day 12 (the first 10 asks) | an ask fails a Check, or complaint fires → You approve each one | POST /{namespace}/actions/{action}/cancel inside the hold |
| Release the reward | It does it | 4 of 4 | — | reversal fires → every release to you | POST /{namespace}/actions/{action}/redirect |
| Hold the reward | To a person, every time | 1 held, 1 voided by you | never | — | POST /{namespace}/tasks/{task}/claim |
| Offer Partner terms · Not now | It does it | 0 · 3 | — | — | POST /{namespace}/actions/{action}/redirect |
| Change the rewards, caps or outcome | You approve each one | 0 merged | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a testimonial in their name, a person named for them to invite, a reward called cash | Refused, every time | 1 ask refused (fact-check) | never | — | — |
What it reads · What it may call. It reads the declared outcome, codes, signups, Stripe payments and refunds; no watched Event appears in an ask, and it never names a Person or company for the referrer to invite. Its tools are Referral.invite (Not live yet, gaps #3), POST /{namespace}/credit-grants/issue, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.
You hold the CSM Key Result: 12 paid referrals per 100 Customers by 2026-12-31. ← worker, metric
Ask once, the day the declared outcome lands; a held Customer gets Not now. ← triggers, chooses, keep-first
Name both rewards beside the link, in words a stranger understands. ← copyRules 1
Never write a referrer’s testimonial or name someone for them to invite. ← copyRules 3, 11
Release a reward only after the invitee’s first payment clears its refund window. ← reward-hold, your file line 3 The slow loop. The rewards and the outcome are fixed in your file, and the weekly Decisions changed no rule option 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 |
|---|---|---|---|---|---|
Ask after the outcome ask-after-outcome | the declared outcome lands | Your rule (line 2) | referral-ask-email: one ask, one reminder, in their thread | the Customer shares, or nothing more sends | the same |
Send their typed invite send-their-invite | a referrer types a peer’s address | Your rule (copyRules 6) | Referral.invite (Not live yet, gaps #3) → referral-typed-invite, naming them and Leaseline | the invitee | the same |
Release the reward release-the-reward | the first payment clears its 30-day window, no match | Your rule (line 3) | POST /{namespace}/credit-grants/issue to both | nobody | the same |
Hold the reward hold-the-reward | a card, device or address match, or a burst | Agent’s call, with its reason | POST /{namespace}/tasks/{task}/assign: you release or void it | you, due 1 business day · Take this one | the same |
Offer Partner terms offer-partner-terms | 5 paid referrals in 90 days | Agent’s call, with its reason | graduation-offer | Partnerships · your agent (commission-partners) | you |
Not now not-now | another Play holds them, or keep-first applies | Your rule (rule j) | nothing; names the holder | their CSM · Take this one | you |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | “Sure. Who should I send it to?” | their code, the share link and the typed-invite box; it never names a person for them | the Customer | — | Referral.invite (Not live yet, gaps #3) |
| Send me pricing | “What would my friend pay?” | the Pricing Page’s list Price, and half off their first month | CSM · your agent | 1 business hour | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Maybe after the holidays.” | the reminder cancels; customer-ask-cap frees them; the code stays on | nobody | at once | GET /{namespace}/decisions |
| Wrong person | “Ask our office manager, she knows everyone.” | asks them to forward it; it never writes to an address from their text | CSM · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply until Oct 28 | the reminder waits for the return date | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Can my friend get 3 months free instead?” | To a person: a different reward is never the agent’s call | you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “No more referral emails, thanks.” | referral asks stop; their account emails keep running | nobody | at once | POST /{namespace}/contacts/{contact}/unsubscribe |
When it hands to you
Held rewards come to you (Dana Okafor, person); a referrer at 5 paid referrals goes to Partnerships · your agent for Partner terms; a held Customer stays with their CSM. One Brief per Role, as captured (brief is Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_689" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-09-30T17:00:00Z" ,
"on": "referral_12",
"brief": {
"who": "the invitee from Juniper Homes’ code",
"chose": "hold-the-reward",
"why": "paid with the same card as Juniper Homes",
"amount": "$29 in credit, both sides" ,
"never": [
"the match named to either side"
]
}
},
{
"$id": "https://api.sb/leaseline.app/tasks/task_692" ,
"$type": "Task",
"assignee": "worker_10",
"due": "2026-10-20T17:00:00Z" ,
"on": "customer_7",
"brief": {
"who": "Birchway Properties",
"chose": "offer-partner-terms",
"why": "4 paid referrals in 90 days; 1 more reaches the line" ,
"held": "waits for the fifth paid referral"
}
}
]Each receiver answers with POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, or POST /{namespace}/actions/{action}/redirect. Once you claim a held reward, nothing releases until you choose release or void. A Task nobody claims by its due time goes up through POST /{namespace}/tasks/{task}/escalate (Partnerships to you), and person-promise fires.
On time: you, 1 of 1 claimed within 1 business day, 2 h 10 min; Partnerships · your agent, 1 of 1; n 2, ■□□□ Too early to tell beside Other Startups’ 95 % on time. Your queue: counted by kind: about 1 held reward a month and 3 asks a week that send on their own.
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 ask to a Customer whose open Deal or Conversation a person owns | Leave it to the person who owns it: the draft waits in their queue. | POST /{namespace}/tasks/{task}/assign |
keep-first (rule j, gaps #8) | a referral ask 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 (gaps #3) | a second open ask to one Customer in a window, from any Play | usage-upsell holds them until Nov 3. | GET /{namespace}/decisions |
fact-check (hub, on POST /{namespace}/content/{content}/publish) | an ask or note that names an outcome the Customer did not have | Cut the line, or name the record that backs it. | POST /{namespace}/content/draft |
invite-bounce (the record’s Invite bounce stop) | a typed invite from a referrer whose invites bounce over 5 % | That referrer shares the link instead of typing addresses. | Referral.invite (Not live yet, gaps #3) |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that sent is the Maple Court thread in Example.
| Check | Fix | Then |
|---|---|---|
fact-check fail · Not live yet, gaps #1 | Cut the line, or name the record that backs it. | POST /{namespace}/content/draft |
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 ask to Maple Court did not send: it names a result they did not have.",
"checks": [
{
"check": "fact-check",
"status": "fail",
"doc": "/plays/customer-referrals#check-fact-check",
"fix": "Cut the line, or name the record that backs it.",
"then": "Content.draft"
},
{
"check": "keep-first",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "burst",
"observed": "the cap hit 3 days running",
"bound": "3 days",
"window": "1 code"
},
"doc": "/plays/customer-referrals#stop-burst" ,
"task": "task_694"
}
]Did it work
At 10 asks a month, the 10 % held back (4 Customers) reads nothing within a year, so the basis is before-after (rule o): nothing prints as Added, and the held-back read pools with Other Startups. Each figure’s call is POST /{namespace}/metrics/query by node, and every node above the leaf is Not live yet (gaps #4).
9.8 paid referrals per 100 Customers against 2.7 before · before-after,
- Chose
- ask-after-outcome
- Why
- Alder Street Rentals sent their first renewal notice 60 days out at 08:10; they sit in the asked 90 %, and no other Play holds them
- From
- ask-after-outcome · send-their-invite · release-the-reward · hold-the-reward · offer-partner-terms · not-now
- Expects
- a referral signup on their code within 30d
- Reopens when
- no signup on customer_21’s code within 7d; one reminder then sends in the thread
- By
- worker_5
JSON
{
"$id": "decisions/decision_684",
"$type": "Decision",
"choice": "ask-after-outcome",
"reason": "Alder Street Rentals sent their first renewal notice 60 days out at 08:10; they sit in the asked 90 %, and no other Play holds them",
"stakes": "low",
"options": [
"ask-after-outcome",
"send-their-invite",
"release-the-reward",
"hold-the-reward",
"offer-partner-terms",
"not-now"
],
"by": "worker_5",
"on": "customer_21",
"expected": "a referral signup on their code within 30d",
"reopensWhen": "no signup on customer_21’s code within 7d; one reminder then sends in the thread"
}| Node | Now (90 days) | Before (the 90 days before) | Added | Confidence | Basis |
|---|---|---|---|---|---|
paid-referrals | 9.8 per 100 Customers (4 paid referrals, 41 Customers) | 2.7 (1 of 37) | — · Worth about +$110 a month | — | before-after, not proof · Other Startups’ results +3.1 per 100 asked, ■■■□ Likely, n 6,800 |
referral-share ↑ | 4 of 11 new paying Customers (36 %) | 1 of 9 (11 %) | — | — | before-after |
new-paying-customers · new-arr | $1,392, 4 Customers · Booked $1,392: credited, not all caused by this Play | $348, 1 | — | — | before-after · 2 invitees still inside their refund window |
net-new-arr · arr | carried from new-arr | carried | — | — | carried |
The leaf counts paid referrals per 100 Customers over 90 days: a referred first Order that cleared its refund window with no match. The held-back 10 % get no card and no email, and keep their code, so the read measures the ask.
How it moves it: the ask lands within the hour the first renewal notice goes out 60 days early, in the Customer’s own thread, with both rewards beside the link.
Guardrails: before against after: logo-churn among Customers asked, 0 of 30 against a trailing 2.1 % a month, and reward cost per paid referral, $29 against the $29 first charge, at the Stop’s line.
{
"$id": "https://api.sb/leaseline.app/campaigns/customer-referrals" ,
"$type": "Campaign",
"play": "play:customer-referrals@1" ,
"path": [
"paid-referrals",
"referral-share",
"new-paying-customers",
"new-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "paid-referrals",
"unit": "per-100",
"heldBack": 4,
"decision": null
},
"readout": {
"value": null,
"n": 4,
"maturing": 2,
"interval": null,
"readability": "inadmissible",
"basis": "before-after",
"pooled": true
},
"ledger": {
"spend": 116,
"workerCost": 2,
"hours": 1.2,
"pipelineArr": 696,
"wonArr": 1392,
"projectedArr": 108,
"paybackDays": 30
}
}Outside benchmark: none recorded for this Play yet; Other Startups’ results print beside every row.
Before it · After it. Before it: usage-upsell hears the Customers it upgraded and review-requests shares the declared outcome. After it: a referrer at 5 paid referrals graduates to commission-partners (referrer-graduates), and instant-response answers invitees who write in.
Example
Leaseline ran it from 2026-07-21 to 2026-10-19 (90 days): 34 Customers reached the outcome, 4 were held back at random, 30 were asked, 8 shared, 9 invitees signed up, 5 paid, 1 refunded and 1 matched its referrer’s card. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Decisions | Done | Paid referrals (Booked: credited, not all caused by this Play) |
|---|---|---|---|
| Ask after the outcome · Send their typed invite | 30 · 6 | 30 asks, 11 reminders · 6 invites | 4 · $1,392 |
| Release the reward · Hold the reward | 4 · 1 | $116 in credit · 1 voided by you | — |
| Offer Partner terms · Not now | 0 · 3 | — · 3 named the holder | — |
| At 10 Customers at the outcome (the Use it when volume) | about 3 asks a month | before-after, not proof | Other Startups’ results: +3.1 per 100 asked, ■■■□ Likely, n 6,800 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_610 Campaign.created | the three steps; the dry run fails billing-connected | 15 min |
| 0 | event_611 Integration.connected | Stripe connected; you approve the terms and the ask once | 15 min |
| 3 | event_640 Action.refused | fact-check refused an ask naming a result Maple Court did not have; the rewrite sent | — |
| 9 | event_702 Order.paid | the first referred Customer pays | — |
| 12 | event_741 Role.updated | promoted: asks send after a 2-minute hold | — |
| 39 | event_902 Monitor.checked | the first refund window clears; both credits release | — |
| 62 | event_1101 Campaign.paused | burst: one code hit its daily cap 3 days running; you voided 1 and resumed | 10 min |
| 1–90 | event_655 Task.assigned, first of 2 | you decided 1 held reward; weekly reviews, from the POST /{namespace}/tasks/next log | 30 min · total 1.2 h measured, 1.5 h asked |
Leaseline → Kestrel Rentals, in their thread, Jul 30, 09:00 · Your first renewal notice went out 60 days before the lease ends.¹ Know another property manager who’d want that? Share your link: they get half off their first month, and you get half off a month once they’ve paid.² Terms are here. *Signed with the Role’s disclosure line.*³
Kestrel, Jul 30, 11:20 · typed one address; the invite named Kestrel and Leaseline. Their peer paid on Aug 8, and both credits released on Sep 8.
- You cut late renewals to zero this month. Tell a friend!
+ Your first renewal notice went out 60 days before the lease ends. Know someone who’d want that?The release run, 2026-09-29 07:00 · the invitee from Juniper Homes’ code paid with Juniper’s card. · CSM · your agent, 07:01 · held both credits and sent you the Brief (
task_689), due Sep 30 17:00; neither side was told of the match. · Dana, Sep 29 09:10 · voided it.
¹ The Customer’s own declared outcome, nothing watched. ² Both rewards beside the link, in plain words. ³ The fixed signature line, compiled from the Role’s disclosure Setting.
Your settings
---
outcome: first renewal notice sent 60 days out
reward: half off one month, both sides, after the 30-day refund window
holdout: 0.1 # 1 in 10 Customers at the outcome gets no ask for 90 days
---
Ask once, the day the outcome lands. Never write their words for them. $ npx startup-builder show plays/customer-referrals
outcome first notice 60 days out · reward half a month, both · holdout 0.1 your file plays/customer-referrals.mdx:2-4,6 (Ask 2026-07-21)
family referral · worker role:success-manager, owns never · chooses 6 (4 rule, 2 read) · sequences referral-ask-email, referral-typed-invite, graduation-offer · checks owned-by-a-person, keep-first, customer-ask-cap, fact-check, invite-bounce · stops 8 · placements gmail, outlook, yahoo-mail, linkedin, x catalog play:customer-referrals@1 (Not live yet, gaps #5) Every key from the catalog
Compiled, not written: edit the Play record customer-referrals 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.