Skip to content

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.

Channel
Reviews and referrals
Run oneMode try
You, to your agent

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

On 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, not proof · n 4 · path → arr

Try another: the CustomerChoiceReason
Alder Street Rentals: first renewal notice sent 60 days out, todayask-after-outcomethe declared outcome; no other Play holds them
The invitee from Juniper Homes’ code paid with Juniper’s cardhold-the-rewarda card match: you decide before anything releases
Kestrel Rentals’ invitee: first payment past its 30-day windowrelease-the-rewardcleared, no match: both credits release
Harbor Row: at risk since Oct 2not-nowkeep-first: their CSM sees them first

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/leaseline.app · 41 Customers at 2026-10-19 · on Outcome.delivered · first ask 52 min after the outcome, inside 09:00–17:00 · 9 keys Not live yet

Start it this week

Terminal
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"}'
422Refusedcustomer-referrals can’t start yet: 1 check fails and 2 warn.
CheckFixThen
use-when pass · Not live yet, gaps #831 Customers reached the first notice sent 60 days outPOST /{namespace}/metrics/query
billing-connected fail · Not live yet, gaps #8Connect Stripe so no credit releases on a refunded or disputed payment.POST /{namespace}/integrations/connect
baseline warn · Not live yet, gaps #8n 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 #8Leave 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.

CheckFix (one call)Your timeWait
use-when · pass: 31 Customers at the outcome (gaps #8)POST /{namespace}/metrics/query on paid-referrals1 minnone
billing-connected · failPOST /{namespace}/integrations/connect with system recurring-billing (Stripe)5 minthe first sync, about 10 min
baseline · warn: 1 in 90 daysPOST /{namespace}/metrics/query; it fills Before in Your number0 minnone
role-unstaffed · warn (gaps #8): 4 of your Customers have a person owner, PriyaPOST /{namespace}/roles/{role}/staff, or leave it: held rewards come to you0 minnone

Your number · Before 2.7 per 100 Customers · Now 9.8 paid referrals per 100 Customers · Target 12 by Dec 31 · before-after, not proof · n 4 paid referrals

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

WhenTrigger idStarts onWindowHow it gets in
A Customer reaches the outcome you declareddelivered-outcomeevent on Outcome.delivered where the Offer’s declared delivered-outcome Event, once per Customer7dyour app records the outcome (POST /{namespace}/events/capture)
A referred Customer’s first Order is paidreferral-first-paymentevent on Order.paid where a referred Customer’s first Order30dPOST /{namespace}/integrations/connect (Stripe)
That payment’s refund window closes cleanrefund-window-closedat on Monitor.checked where the end of the refund window on a referred first Order, with no refund or Dispute7dthe daily release run
+ 5 more for signups on a code, reversals, bursts, graduations and public pages carrying a code (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$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
  }
}
Reproduce it in test
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:success-manager, 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.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Ask after the outcome · Send their typed inviteIt does it, you can stop it30 of 30 · 6 of 6met on day 12 (the first 10 asks)an ask fails a Check, or complaint fires → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Release the rewardIt does it4 of 4—reversal fires → every release to youPOST /{namespace}/actions/{action}/redirect
Hold the rewardTo a person, every time1 held, 1 voided by younever—POST /{namespace}/tasks/{task}/claim
Offer Partner terms · Not nowIt does it0 · 3——POST /{namespace}/actions/{action}/redirect
Change the rewards, caps or outcomeYou approve each one0 mergednever—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 cashRefused, every time1 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.

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

OptionPicked whenDecided byRunsHands toIf it’s just you
Ask after the outcome ask-after-outcomethe declared outcome landsYour rule (line 2)referral-ask-email: one ask, one reminder, in their threadthe Customer shares, or nothing more sendsthe same
Send their typed invite send-their-invitea referrer types a peer’s addressYour rule (copyRules 6)Referral.invite (Not live yet, gaps #3) → referral-typed-invite, naming them and Leaselinethe inviteethe same
Release the reward release-the-rewardthe first payment clears its 30-day window, no matchYour rule (line 3)POST /{namespace}/credit-grants/issue to bothnobodythe same
Hold the reward hold-the-rewarda card, device or address match, or a burstAgent’s call, with its reasonPOST /{namespace}/tasks/{task}/assign: you release or void ityou, due 1 business day · Take this onethe same
Offer Partner terms offer-partner-terms5 paid referrals in 90 daysAgent’s call, with its reasongraduation-offerPartnerships · your agent (commission-partners)you
Not now not-nowanother Play holds them, or keep-first appliesYour rule (rule j)nothing; names the holdertheir CSM · Take this oneyou
When they write backFixture replyWhat happensWho answersBy whenCall
Interested“Sure. Who should I send it to?”their code, the share link and the typed-invite box; it never names a person for themthe 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 monthCSM · your agent1 business hourPOST /{namespace}/conversations/{conversation}/answer
Not now“Maybe after the holidays.”the reminder cancels; customer-ask-cap frees them; the code stays onnobodyat onceGET /{namespace}/decisions
Wrong person“Ask our office manager, she knows everyone.”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 28the reminder waits for the return datenobodythe return datePOST /{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 callyou1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“No more referral emails, thanks.”referral asks stop; their account emails keep runningnobodyat oncePOST /{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).

What lands in each queue · sample
[
  {
    "$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)RefusesFixThen
owned-by-a-person (rule j; Not live yet, gaps #8)an ask to a Customer whose open Deal or Conversation a person ownsLeave 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 leavingLet 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 Playusage-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 haveCut 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)
Stop (pauses the Play)Pauses whenThen
reversalrefunds and Disputes on referred first Orders over twice your rate, over 30 paid referralsreferrer rewards pause
fraud-shareheld or voided referrals over 20 % of rewarded signups, over 50invitee rewards pause; a Decision opens
bursta code hits its daily cap 3 days in a rowits rewards pause and hold
costreward cost per paid referral over the referred Customer’s first charge, over 30asks pause; a Decision opens
yield-floorunder 1 paid referral per 100 asked, after 300 asks and 90 daysthe run stops; a Decision opens
complaintspam complaints near 0.1 % of a day’s asks or invitesthat send type pauses on every Mailbox
bouncebounces over 2 % of a day’s sendsthe Mailbox pauses
kill-switchyou press itevery ask pauses; codes keep working
+ 2 more for the social lane: a share text that drops the reward line, and Venue warnings (?lanes=all)

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.

422RefusedThe ask to Maple Court did not send: it names a result they did not have.
CheckFixThen
fact-check fail · Not live yet, gaps #1Cut 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.

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "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, not proof · n 4 · path → arr

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"
}
NodeNow (90 days)Before (the 90 days before)AddedConfidenceBasis
paid-referrals9.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 · arrcarried from new-arrcarried——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.

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

What happened, by optionDecisionsDonePaid referrals (Booked: credited, not all caused by this Play)
Ask after the outcome · Send their typed invite30 · 630 asks, 11 reminders · 6 invites4 · $1,392
Release the reward · Hold the reward4 · 1$116 in credit · 1 voided by you—
Offer Partner terms · Not now0 · 3— · 3 named the holder—
At 10 Customers at the outcome (the Use it when volume)about 3 asks a monthbefore-after, not proofOther Startups’ results: +3.1 per 100 asked, ■■■□ Likely, n 6,800
DayEventWhat happenedYour time
0event_610 Campaign.createdthe three steps; the dry run fails billing-connected15 min
0event_611 Integration.connectedStripe connected; you approve the terms and the ask once15 min
3event_640 Action.refusedfact-check refused an ask naming a result Maple Court did not have; the rewrite sent—
9event_702 Order.paidthe first referred Customer pays—
12event_741 Role.updatedpromoted: asks send after a 2-minute hold—
39event_902 Monitor.checkedthe first refund window clears; both credits release—
62event_1101 Campaign.pausedburst: one code hit its daily cap 3 days running; you voided 1 and resumed10 min
1–90event_655 Task.assigned, first of 2you decided 1 held reward; weekly reviews, from the POST /{namespace}/tasks/next log30 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.

Maple Court, 2026-07-24 · the ask fact-check refused, then the rewrite that sent
- 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

plays/customer-referrals.mdx
---
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.
Text
$ 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

KeyIdWhat it is
Tacticreferral-termsReferral terms set the invitee and referrer rewards, the caps and the expiry for each Offer, priced below the referred Customer’s first charge. Starts at You approve the first wave.
Tacticreferral-askReferral ask invites each Customer who reaches the declared delivered-outcome Event to share their code, at most once in 90 days, with both rewards named. Starts at You approve each one.
Tacticreferral-shareReferral link share gives each Customer one coded link and a share menu that carries the reward disclosure, sent from the referrer’s own accounts. Starts at You approve the first wave.
Tacticreferral-attributionReferral attribution credits a signup to a Customer’s code only when the code arrives with it, applies the caps, and issues the invitee’s reward. Starts at Sends nothing.
Tacticreward-holdReward hold keeps each referral reward pending until the refund window closes and voids it on a reversal or hard fraud flag. Starts at You approve each one.
Tacticdisclosure-watchDisclosure watch asks each public page that sends coded arrivals to show the reward statement, and stops rewards from pages without it. Starts at Sends nothing.
Tacticreferrer-graduationReferrer graduation offers commission terms to a Customer whose referrals prove out, signs the standard Agreement, and moves their code to commission-partners. Starts at You approve each one.
Tacticreferral-readoutReferral readout computes paid referrals per 100 Customers against the holdout and the reward cost, and opens one Decision each month. Starts at Sends nothing.
Triggerdelivered-outcomeDelivered outcome: event on Outcome.delivered, window 7d.
Triggerreferral-signupReferral signup: event on Visitor.identified, window 1d.
Triggerreferral-first-paymentReferral first payment: event on Order.paid, window 30d.
Triggerrefund-window-closedRefund window closed: at on Monitor.checked, window 7d.
Triggerreferral-reversalReferral reversal: event on Order.refunded, window 1d.
Triggerreferral-burstReferral burst: threshold on Monitor.checked, window 1d.
Triggergraduation-thresholdGraduation threshold: threshold on Monitor.checked, window 30d.
Triggerpublic-referral-pagePublic referral page: threshold on Monitor.checked, window 14d.
Sequencereferral-ask-emailReferral ask email: 2 steps; it ends on The Customer takes a share action.; The Customer replies.; The Customer opts out or is suppressed.; 14 days pass from the ask..
Sequencereferral-typed-inviteReferral typed invite: 1 steps; it ends on The invite is sent..
Sequencegraduation-offerGraduation offer: 2 steps; it ends on The referrer accepts.; The referrer declines or replies.; The referrer opts out or is suppressed.; 14 days pass from the offer..

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.