URL: /plays/customer-referrals · Id: `play:customer-referrals`

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

Asks [Customers](https://startupbuilder.dev/business/nouns/customer) to invite a peer right after they get the result they came for, and rewards both once the peer’s first payment clears refunds.

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

```text title="Run one · Ask"
Hand our latest Customer who reached the outcome 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 ("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 }
```

```json title="Run one · Response · sample"
{
  "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"
    ],
    "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"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_5",
      "verb": "Coupon.create",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$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": "fact-check",
          "status": "pass"
        }
      ]
    }
  ]
}
```

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

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$110 ARR a month at 10 asked a month | 20 min week 1, then 10 min a week | $29 in credit per paid referral, ~$40 a month | Day 9: the first referred Customer pays | 10+ Customers at your declared outcome, Stripe connected | Under 10 at the outcome: `review-requests` · no outcome declared: declare one (<Op verb="Metric.define" />) |

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


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

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-use-when"></a>`use-when` · pass: 31 Customers at the outcome (gaps #8) | <Op verb="Metric.query" /> on `paid-referrals` | 1 min | none |
| <a id="check-billing-connected"></a>`billing-connected` · 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: 1 in 90 days | <Op verb="Metric.query" />; it fills Before in Your number | 0 min | none |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn (gaps #8): 4 of your Customers have a person owner, Priya | <Op verb="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, 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
| 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 (<Op verb="Event.capture" />) |
| A referred Customer’s first Order is paid | `referral-first-payment` | `event` on `Order.paid` where a referred Customer’s first Order | 30d | <Op verb="Integration.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=all`) | | | | |

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

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

| 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 | <Op verb="Action.cancel" /> inside the hold |
| Release the reward | It does it | 4 of 4 | — | `reversal` fires → every release to you | <Op verb="Action.redirect" /> |
| Hold the reward | To a person, every time | 1 held, 1 voided by you | never | — | <Op verb="Task.claim" /> |
| Offer Partner terms · Not now | It does it | 0 · 3 | — | — | <Op verb="Action.redirect" /> |
| Change the rewards, caps or outcome | You approve each one | 0 merged | never | — | <Op verb="Change.merge" /> · <Op verb="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), <Op verb="CreditGrant.issue" />, <Op verb="Task.assign" />, and any other call becomes an Approval under the Role’s Mandate.

```md title="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
| 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) | <Op verb="CreditGrant.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 | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Not now | “Maybe after the holidays.” | the reminder cancels; `customer-ask-cap` frees them; the code stays on | nobody | at once | <Op verb="Decision.list" /> |
| 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 | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply until Oct 28 | the reminder waits for the return date | nobody | the return date | <Op verb="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 | <Op verb="Conversation.escalate" /> |
| Stop | “No more referral emails, thanks.” | referral asks stop; their account emails keep running | nobody | at once | <Op verb="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).

```json title="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 <Op verb="Task.claim" /> (Take this one), <Op verb="Task.return" /> with a reason, or <Op verb="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 <Op verb="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:** <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 |
| --- | --- | --- | --- |
| <a id="check-owned-by-a-person"></a>`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. | <Op verb="Task.assign" /> |
| <a id="check-keep-first"></a>`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. | <Op verb="Task.next" /> |
| <a id="check-customer-ask-cap"></a>`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. | <Op verb="Decision.list" /> |
| <a id="check-fact-check"></a>`fact-check` (hub, on <Op verb="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. | <Op verb="Content.draft" /> |
| <a id="check-invite-bounce"></a>`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 when | Then |
| --- | --- | --- |
| <a id="stop-reversal"></a>`reversal` | refunds and Disputes on referred first Orders over twice your rate, over 30 paid referrals | referrer rewards pause |
| <a id="stop-fraud-share"></a>`fraud-share` | held or voided referrals over 20 % of rewarded signups, over 50 | invitee rewards pause; a Decision opens |
| <a id="stop-burst"></a>`burst` | a code hits its daily cap 3 days in a row | its rewards pause and hold |
| <a id="stop-cost"></a>`cost` | reward cost per paid referral over the referred Customer’s first charge, over 30 | asks pause; a Decision opens |
| <a id="stop-yield-floor"></a>`yield-floor` | under 1 paid referral per 100 asked, after 300 asks and 90 days | the run stops; a Decision opens |
| <a id="stop-complaint"></a>`complaint` | spam complaints near 0.1 % of a day’s asks or invites | that send type pauses on every Mailbox |
| <a id="stop-bounce"></a>`bounce` | bounces over 2 % of a day’s sends | the Mailbox pauses |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | every 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.

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


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

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

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

```diff title="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
```mdx title="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
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-referral-terms"></a>`referral-terms` | Referral 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. |
| Tactic | <a id="tactic-referral-ask"></a>`referral-ask` | Referral 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. |
| Tactic | <a id="tactic-referral-share"></a>`referral-share` | Referral 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. |
| Tactic | <a id="tactic-referral-attribution"></a>`referral-attribution` | Referral 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. |
| Tactic | <a id="tactic-reward-hold"></a>`reward-hold` | Reward 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. |
| Tactic | <a id="tactic-disclosure-watch"></a>`disclosure-watch` | Disclosure 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. |
| Tactic | <a id="tactic-referrer-graduation"></a>`referrer-graduation` | Referrer 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. |
| Tactic | <a id="tactic-referral-readout"></a>`referral-readout` | Referral readout computes paid referrals per 100 Customers against the holdout and the reward cost, and opens one Decision each month. Starts at Sends nothing. |
| Trigger | <a id="trigger-delivered-outcome"></a>`delivered-outcome` | Delivered outcome: `event` on `Outcome.delivered`, window 7d. |
| Trigger | <a id="trigger-referral-signup"></a>`referral-signup` | Referral signup: `event` on `Visitor.identified`, window 1d. |
| Trigger | <a id="trigger-referral-first-payment"></a>`referral-first-payment` | Referral first payment: `event` on `Order.paid`, window 30d. |
| Trigger | <a id="trigger-refund-window-closed"></a>`refund-window-closed` | Refund window closed: `at` on `Monitor.checked`, window 7d. |
| Trigger | <a id="trigger-referral-reversal"></a>`referral-reversal` | Referral reversal: `event` on `Order.refunded`, window 1d. |
| Trigger | <a id="trigger-referral-burst"></a>`referral-burst` | Referral burst: `threshold` on `Monitor.checked`, window 1d. |
| Trigger | <a id="trigger-graduation-threshold"></a>`graduation-threshold` | Graduation threshold: `threshold` on `Monitor.checked`, window 30d. |
| Trigger | <a id="trigger-public-referral-page"></a>`public-referral-page` | Public referral page: `threshold` on `Monitor.checked`, window 14d. |
| Sequence | <a id="sequence-referral-ask-email"></a>`referral-ask-email` | Referral 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.. |
| Sequence | <a id="sequence-referral-typed-invite"></a>`referral-typed-invite` | Referral typed invite: 1 steps; it ends on The invite is sent.. |
| Sequence | <a id="sequence-graduation-offer"></a>`graduation-offer` | Graduation 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.. |