URL: /plays/product-led · Id: `play:product-led`

# Product-led growth

> Gives free Customers enough to reach the result twice, then shows the upgrade inside the task at the Limit, with the real Price.

Gives free [Customers](https://startupbuilder.dev/business/nouns/customer) enough to reach the result twice, then shows the upgrade inside the task at the [Limit](https://startupbuilder.dev/business/nouns/limit), with the real [Price](https://startupbuilder.dev/business/nouns/price).

```text title="Set it up · Ask"
Be our CSM: give free accounts enough lease reads to send their first notices twice, then show Pro at the limit, inside the task, with the real price. Over 150 units, bring in our AE. Keep half on today’s free plan. Show me what’s missing; don’t send.
```

```text title="Run one · Ask"
Hand our latest free account at a limit 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 ("Entitlement.checked", "Event.captured", "Customer.activated", "Offer.subscribed", "Subscription.ended", "Version.deployed", "Metric.watched")', 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_361",
    "$type": "Decision",
    "choice": "prompt-at-bound",
    "reason": "the 10th lease read this month was refused, inside the renewal notice they were drafting, and Pro grants 200",
    "stakes": "low",
    "options": [
      "prompt-at-bound",
      "offer-paid-trial",
      "upsell-month-one",
      "win-back",
      "to-the-ae",
      "not-now"
    ],
    "by": "worker_5",
    "on": "customer_44",
    "expected": "Offer.subscribed on Pro within 14d",
    "reopensWhen": "no Offer.subscribed for customer_44 within 14d"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_5",
      "verb": "Entitlement.check",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_5",
      "verb": "Flag.evaluate",
      "checks": []
    },
    {
      "$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_2` puts Oakfield in the agent half; the prompt shows inside the notice they were drafting. `action_3` is the bound follow-up’s day-1 email, which ends when they upgrade or the grant resets.

`+18.3 activated per 100 · ■■□□ Early sign · n 164 · path → arr`

| Try another: the free account | Choice | Reason |
| --- | --- | --- |
| Oakfield Lettings: 10 of 10 free lease reads used, the 10th refused mid-notice | `prompt-at-bound` | the bound met inside the task |
| Pinecrest Rentals: 14 notices sent in 6 days on free, no bound met | `offer-paid-trial` | the pattern that pays, before any limit |
| Juniper Homes: paid 12 days ago, a second member refused the owner report | `upsell-month-one` | Team grants the owner report |
| Cedar Lane: ended Sep 30; the reminder fix they asked for shipped Oct 14 | `win-back` | the reason they gave is fixed |
| Northgate Residential: 220 units on a free account, asked about a contract | `to-the-ae` | over 150 units, and terms are the AE’s |

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$1,500 ARR a month at 99 | 35 min week 1, then 10 a week | $0.21 per free signup, ~$21 a month at 99 | Day 3: first upgrade paid | 20+ free signups a month, a metered Limit | Under 20: `signup-interviews` · no metered Limit: add one (<Op verb="Offer.publish" />) |

**Where it sits:** Stranger → Lead → **Trialing** → **Activated** → At risk → **Churned** → Won back · Activation rate → New paying Customers → New Customers ↑ → ARR added each week → ARR

Worth’s formula: about +18.3 activated per 100 × 99 free signups a month × 0.24 activated to paid (your last 90 days) × $347 Pro ARR ≈ +$1,509. Under First signal: About 6 weeks to tell a +14 lift at 99 a month (4 to read, 2 to mature); at 20 a month, 90 days can tell only +31 or more, so your file starts at `holdout: 0`.

`sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Entitlement.checked · first prompt 0.4 s · 8 keys Not live yet`

## Start it this week
```bash
npx startup-builder add play product-led   # 1 · writes plays/product-led.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":"product-led"}'

# 3 · keep it, at You approve the first wave
curl -X POST https://api.sb/leaseline.app/campaigns \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"play":"product-led"}'

# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/product-led \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":null}'

# off: no prompt or email sends; free grants and the cancel button stay
curl -X PATCH https://api.sb/leaseline.app/campaigns/product-led \
  -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": "product-led can’t start yet: 1 check fails and 1 warns.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "99 free signups in 30 days",
      "doc": "/plays/product-led#check-use-when",
      "fix": "Keep 20 or more free signups a month, or run signup-interviews.",
      "then": "Metric.query"
    },
    {
      "check": "limit-on-offer",
      "status": "fail",
      "observed": "Leaseline Free: 5 units, lease reads unmetered",
      "doc": "/plays/product-led#check-limit-on-offer",
      "fix": "Add a 10-read monthly Limit to Leaseline Free, then publish it.",
      "then": "Offer.publish"
    },
    {
      "check": "billing-connected",
      "status": "pass",
      "observed": "Stripe, connected 2026-08-24",
      "doc": "/plays/product-led#check-billing-connected",
      "fix": "Connect Stripe so every grant, charge and Meter reading is live.",
      "then": "Integration.connect"
    },
    {
      "check": "baseline",
      "status": "pass",
      "observed": "31.7 activated per 100 free signups, n 212 in 90 days",
      "doc": "/plays/product-led#check-baseline",
      "fix": "Read it once; it fills Before in Your number.",
      "then": "Metric.query"
    },
    {
      "check": "role-unstaffed",
      "status": "warn",
      "roles": [
        "success-manager"
      ],
      "owned": 2,
      "doc": "/plays/product-led#check-role-unstaffed",
      "fix": "Staff the CSM, or leave it: CSM routes come to you, and Priya’s 2 get drafts.",
      "then": "Role.staff"
    }
  ]
}
```

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-use-when"></a>`use-when` · <a id="check-baseline"></a>`baseline` · pass: 99 free signups a month; 31.7 activated per 100, n 212 (gaps #8) | <Op verb="Metric.query" /> on `activation-rate`, which fills Before in Your number | 1 min | none |
| <a id="check-limit-on-offer"></a>`limit-on-offer` · fail (gaps #8): lease reads on Leaseline Free have no Limit | <Op verb="Offer.publish" /> with a 10-read monthly Limit, 2 a day | 10 min | none; new signups get it, existing free accounts after 30 days’ notice |
| <a id="check-billing-connected"></a>`billing-connected` · pass: Stripe | <Op verb="Integration.connect" /> with `system` recurring-billing (Stripe) | 0 min | none |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn (gaps #8): 2 free accounts have a person owner, Priya | <Op verb="Role.staff" />, or leave it: CSM routes come to you, and Priya’s 2 get drafts in her queue | 0 min | none |

`Your number · Before 31.7 · Now 50.0 activated per 100 · Target 55 by Dec 31 · ■■□□ Early sign · n 164 matured`

Each lease read on a free grant costs $0.03 from the free Offer pool, booked as marketing spend; each prompt email costs $0.04 from your Domain (Worker cost); <Op verb="Entitlement.check" /> costs nothing. That comes to about $21 a month at 99 free signups, inside the $50 pool, and $4 at 20.

## When it starts
| When | Trigger id | Starts on | Window | How it gets in |
| --- | --- | --- | --- | --- |
| A free account meets a metered limit, or has 20 % or less left | `usage-threshold` | `threshold` on `Entitlement.checked` where allowed = false, or 20 % or less of the grant left | prompt inside the task; follow-up within 2d | your app calls <Op verb="Entitlement.check" /> before each lease read |
| A free account does what paying accounts did in their first week | `paid-predictor` | `threshold` on `Event.captured` where the fitted usage Event reaches its fitted count within 7 days of signup | 7d from signup | your app calls <Op verb="Event.capture" /> on each notice sent |
| A new paying account nears a Limit, or a member is refused | `limit-pressure` | `threshold` on `Entitlement.checked` where used / bound >= 0.8 inside the first 30 paid days, or a member refused a Verb the next Offer grants | 3d, inside the first 30 paid days | the same <Op verb="Entitlement.check" /> |
| + 6 more for activation, first paid, cancels, fixes, cost shifts and the pool (`?lanes=all`) | | | | |

```json title="One signed delivery · StartupBuilder-Signature"
{
  "$id": "https://api.sb/leaseline.app/events/event_1310",
  "$type": "Event",
  "name": "Entitlement.checked",
  "at": "2026-10-19T10:12:40Z",
  "seq": 1310,
  "data": {
    "customer": "customer_44",
    "offer": "leaseline-free",
    "limit": "lease-reads",
    "used": 10,
    "bound": 10,
    "allowed": false,
    "resets": "2026-11-01"
  }
}
```

```bash title="Reproduce it in test"
npx startup-builder trigger Entitlement.checked --like event_1310   # replays the delivery
npx startup-builder clock advance --by 1d   # runs the bound follow-up’s day-1 email (action_3)
npx startup-builder listen Entitlement.checked --forward-to http://localhost:3000/hook
```

## Who runs it
**CSM · your agent** (`role:success-manager`, `worker_5`) holds the Key Result. It hands to **AE · Priya Shah (person)** (`role:sales-lead`, `worker_3`) for free accounts over 150 units and any ask for terms, and to **you** (Dana Okafor, person, `worker_1`) for the rest, because no person holds the CSM seat. Your number links to Did it work.

| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
| --- | --- | --- | --- | --- | --- |
| Prompt at the limit · Upsell in month one | It does it, you can stop it | 38 of 38 · 6 of 6 | met on day 9 (the first wave of 20) | any prompt fails a Check → You approve the first wave | <Op verb="Action.cancel" /> inside the hold |
| Offer the paid trial · Win back | You approve the first wave | 19 of 20 (1 refused) · 4 of 4 | at 20 passed in a row | — | <Op verb="Approval.approve" /> · <Op verb="Approval.refuse" /> |
| To the AE · Not now | It does it | 1 of 1 · 3 of 3 | — | a held account is asked anyway → You approve each one | <Op verb="Action.redirect" /> |
| Change the free grant | You approve each one | 1 merged of 1 | never | — | <Op verb="Change.merge" /> · <Op verb="Change.revert" /> |
| Never: a discount, a countdown that isn’t real, a cancel that waits | To a person, every time | 2 asked, 2 handed over | never | — | <Op verb="Approval.approve" /> on the AE’s Quote |

**What it reads · What it may call.** It reads the account’s own Events, the Entitlement and the live Price; a predictor score or a matched company never appears in what it writes. The cancel is one press and never waits on the agent (`exit-never-waits`). Its tools are <Op verb="Entitlement.check" />, <Op verb="Offer.trial" />, <Op verb="Offer.subscribe" />, <Op verb="Conversation.escalate" />, <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:product-led@1 · each line names its record key"
You hold the CSM Key Result: 55 activated per 100 free signups by 2026-12-31.    ← worker, metric
On each Trigger, choose one option and say why; a held account gets Not now.     ← triggers, chooses, keep-first
Free grant: 10 lease reads a month, twice what activation takes.                 ← your file, line 4
A prompt states the units left, the reset time and Pro’s price from the Pricing Page. ← copyRules 1
Never a countdown, a discount or a win-back offer below list.                    ← copyRules 4, 8
Never write to an account a person owns: draft it into their queue.              ← worker.owns, owned-by-a-person
```

**The slow loop, as a Change.** `decision_142` (2026-09-07, weekly) read that activation took a median of 5 lease reads and that 31 of 40 free accounts hit the old 6-read cap before their second notice, so `change_19` raised the grant to 10 (your file, line 4) and Dana merged it with <Op verb="Change.merge" />. **Replayed on past signups**, <Op verb="Change.review" /> put the agent half at 48.1 per 100; read back at +30d it is 50.0, n 82, ■■□□ Early sign (Example, day 44).

**Bring your own agent** (folded) starts at It does it, you can stop it when it matches or beats the seated agent on the eval suite, and at You approve the first wave when it does not; <Op verb="Role.staff" /> seats it.

```bash
npx startup-builder eval --scenario product-led-184 --agent "claude -p" --seat success-manager   # seated: 50.0 per 100, 0 Checks failed sent, $0.21 a signup · yours: 47.6, 1 failed, $0.26 → You approve the first wave
```

## What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
| --- | --- | --- | --- | --- | --- |
| Prompt at the limit `prompt-at-bound` | a metered limit met, or 20 % or less left | Your rule (line 4) | <Op verb="Entitlement.check" /> → the prompt inside the task → `bound-follow-up` | the Customer’s press | the same |
| Offer the paid trial `offer-paid-trial` | the pattern that pays, before any limit | Agent’s call, with its reason | `trial-to-paid` → <Op verb="Offer.trial" /> → `convert-trial` | the Customer’s press | the same |
| Upsell in month one `upsell-month-one` | paid under 30 days, at 80 % of a Limit or a member refused | Your rule (line 5) | `first-30-upsell` → on press `change-plan` | the Customer’s press | the same |
| Win back `win-back` | a canceled plan ended, or their reason got fixed | Agent’s call, with its reason | `win-back-1-30-60` → on press <Op verb="Offer.subscribe" /> | nobody; restore is one press | the same |
| To the AE `to-the-ae` | over 150 units, or an ask for terms | Agent’s call, with its reason | <Op verb="Conversation.escalate" /> → <Op verb="Task.assign" /> | <Op verb="Task.assign" /> → AE · Priya Shah, due 1 business day · Take this one | you, within 1 business day |
| Not now `not-now` | another Play holds them, or `keep-first` applies | 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 | “OK, move us to Pro.” | the one-press link in the prompt; nothing is charged until they press | the Customer | — | <Op verb="Subscription.switch" /> (theirs) |
| Send me pricing | “What does Pro cost for 30 units?” | Pro’s live Price and what it grants, from the Pricing Page | CSM · your agent | 5 min | <Op verb="Conversation.answer" /> |
| Not now | “We’ll stay on free for now.” | the follow-up stops; the grant resets on the 1st as stated | nobody | at once | <Op verb="Decision.list" /> |
| Wrong person | “Our office manager pays for software.” | 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 27 | the follow-up waits for the return date | nobody | the return date | <Op verb="Message.schedule" /> |
| Question it can’t answer | “Can you import our leases from the old system?” | To a person, the question in the Brief | you | 1 business day | <Op verb="Conversation.escalate" /> |
| Stop | “Stop emailing me about plans.” | emails stop; the in-task prompt keeps showing at the limit | nobody | at once | <Op verb="Contact.suppress" /> |

## When it hands to you
To the AE goes to **AE · Priya Shah (person)**; Not now and every approval come to **you**, the CSM seat. 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_441",
    "$type": "Task",
    "assignee": "worker_3",
    "due": "2026-10-07T16:20:00Z",
    "on": "customer_58",
    "brief": {
      "who": "Northgate Residential, free account",
      "said": "We have 220 units. Is there an annual contract?",
      "chose": "to-the-ae",
      "why": "over 150 units; terms are the AE’s",
      "never": [
        "a discount",
        "a deadline that isn’t real"
      ]
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/tasks/task_418",
    "$type": "Task",
    "assignee": "worker_1",
    "due": "2026-09-30T17:00:00Z",
    "on": "customer_51",
    "brief": {
      "who": "Willow Park, Pro since Sep 21",
      "chose": "not-now",
      "why": "2 support threads on failed imports this week",
      "held": "keep-first"
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_52",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-08-28T15:00:00Z",
    "draft": "You’ve sent 14 notices this week. Pro gives you 200 lease reads a month for $29…",
    "route": "offer-paid-trial",
    "checks": [
      {
        "check": "price",
        "status": "pass"
      },
      {
        "check": "keep-first",
        "status": "pass"
      }
    ]
  }
]
```

The receiver 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. Once a person claims, the agent’s pending asks to that account cancel with `claimedByPerson` (Not live yet, gaps #7), and every reply goes to the claimer. A Task nobody claims by its due time goes up a Role through <Op verb="Task.escalate" />, and `person-promise` fires.

**On time:** AE · Priya Shah, 1 of 1 within 1 business day, n 1; you, 3 of 3, median 2 h, n 3, ■□□□ Too early to tell beside Other Startups’ 92 % on time. **Your queue:** in week 1, 20 approvals and 1 weekly review; after the promotion on day 9, 1 weekly review and about 1 hand-off 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 |
| --- | --- | --- | --- |
| <a id="check-owned-by-a-person"></a>`owned-by-a-person` (rule j; gaps #8) | a prompt email to an account whose open Deal a person owns, or whose Conversation a person claimed | 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) | an upgrade or win-back ask to a Customer at risk, past due or with an unsubscribe pending | 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 2. | <Op verb="Decision.list" /> |
| <a id="check-price"></a>`price` (gaps #1) | a price, plan or date that differs from the Pricing Page | Quote the Pricing Page’s live Price, or send the ask to the AE. | <Op verb="Approval.approve" /> |
| <a id="check-exit-never-waits"></a>`exit-never-waits` (the record’s Cancel never waits; gaps #3) | any screen, hold or Approval in front of a cancel, pause or downgrade | Remove the step: the cancel completes in one press. | <Op verb="Subscription.unsubscribe" /> |
| <a id="check-abuse"></a>`abuse` (the record’s Abuse stop) | the costly grant to a cluster of more than 3 free accounts in 7 days, or a disposable mailbox | Verify the mailbox or add a card; cheap Verbs keep working. | <Op verb="PaymentSource.add" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-pool"></a>`pool` | month-to-date free-grant cost reaches 100 % of the $50 pool | new visitors see the trial Offer until the 1st; accepted grants stand |
| <a id="stop-activation-floor"></a>`activation-floor` | a grant change drops activation 3 points or more below the prior 30 days, at n 100 | the change reverts for new signups (<Op verb="Change.revert" />) |
| <a id="stop-downgrade"></a>`downgrade` | an upsell arm’s 60-day downgrade or refund rate passes your half’s | that arm ends |
| <a id="stop-complaint"></a>`complaint` | complaints near 0.1 % of a week’s prompt emails | emails pause on every Mailbox; in-task prompts keep showing |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | every prompt and email stops; new visitors see the control Offer, accepted Offers stand |
| + 3 more for the checkout lane: refund, day-0 cancel and first charge (`?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 Juniper Homes thread in Example.

```json title="The captured refusal · sample"
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The upsell to Juniper Homes did not send: it quotes Team at $59, and the Pricing Page says $79.",
  "checks": [
    {
      "check": "price",
      "status": "fail",
      "doc": "/plays/product-led#check-price",
      "fix": "Quote the Pricing Page’s live Price, or send the ask to the AE.",
      "then": "Approval.approve"
    },
    {
      "check": "keep-first",
      "status": "pass"
    }
  ]
}
```


```json title="The Stops that fired, as Campaign.paused reasons · sample"
[
  {
    "reason": {
      "limit": "pool",
      "observed": "$50.00",
      "bound": "$50",
      "window": "2026-09"
    },
    "doc": "/plays/product-led#stop-pool",
    "task": "task_430"
  }
]
```

## Did it work
Both halves are read from the same random assignment at signup (rules m–o), and 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).

`+18.3 activated per 100 · ■■□□ Early sign · n 164 · path → arr`

| Node | Agent half | Your half | Added | Confidence | Basis |
| --- | --- | --- | --- | --- | --- |
| `activation-rate` | 50.0 per 100 (41 of 82) | 31.7 (26 of 82) | +18.3 [3.5, 33.1] | ■■□□ Early sign | held back at random · n 164 · 20 still maturing |
| `new-paying-customers` | 17.2 per 100 (11 of 64) | 7.8 (5 of 64) | +9.4 [−2.0, 20.7] · Not carrying up yet | ■□□□ Too early to tell | the same assignment, paid by day 30 · n 128 · 36 still maturing |
| `new-arr` ↑ | $3,817, 11 Customers on Pro · Booked: credited, not all caused by this Play | $1,735, 5 Customers | +$2,082 [−$437, +$4,601] · Worth about +$1,500 a month | ■□□□ Too early to tell (11 vs 5 Customers) | the same · 3 trials still deciding |
| `net-new-arr` · `arr` | +$3,817 | +$1,735 | +$2,082, carried: it moves no other bridge term | ■□□□ Too early to tell | carried from `new-arr` |

The leaf counts free signups who sent 2 notices within 14 days per 100 assigned once quarantine has run (rule m), to 150 matured in each half; at 82 and 82 so far the Experiment runs on.

**How it moves it:** the agent half got a metered grant sized to activation (6 reads, then 10 from day 14) and met the upgrade inside the task at a median of day 6; your half kept today’s free plan and its plain upgrade page.

**Guardrails:** treated against held back: paid per activated account, 0.27 against 0.19 (`new-paying-customers` over `activation-rate`), and refunds in 30 days, 1 of 11 against 0 of 5.

```json title="The Campaign card · sample"
{
  "$id": "https://api.sb/leaseline.app/campaigns/product-led",
  "$type": "Campaign",
  "play": "play:product-led@1",
  "path": [
    "activation-rate",
    "new-paying-customers",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "activation-rate",
    "unit": "per-100",
    "heldBack": 92,
    "decision": null
  },
  "readout": {
    "value": 18.3,
    "n": 164,
    "maturing": 20,
    "interval": [
      3.5,
      33.1
    ],
    "readability": "C",
    "basis": "held-back",
    "pooled": false
  },
  "ledger": {
    "spend": 38,
    "workerCost": 6,
    "hours": 1.6,
    "pipelineArr": 2400,
    "wonArr": 3817,
    "projectedArr": 1509,
    "paybackDays": 4
  }
}
```

Outside benchmark: 0.75 % to 0.83 % of Subscriptions upgrade in their first month (a subscription-analytics vendor’s purchase data, 2026-09-30).

**Before it · After it.** Before it: `programmatic-seo`, `editorial-seo` and `free-tools` bring the free signups. After it: `usage-upsell` hears Pro accounts that pace past 50 units, and `run-beside` holds a new paying Customer’s first 30 days; `keep-first` holds an account at risk.

## Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days). 196 free accounts signed up, 12 were quarantined by the abuse Check, and 184 were assigned at random, 92 to each half; 164 have matured for activation and 128 for paid. Read every captured thread at `?threads=all`; three follow the tables.

| What happened, agent half, by option | Decisions | Paid | Won (credited, not all caused by this Play) |
| --- | --- | --- | --- |
| Prompt at the limit | 38 | 7 to Pro | $2,429 ARR |
| Offer the paid trial | 19 | 4 to Pro, 3 still deciding | $1,388 ARR |
| Upsell in month one · Win back · To the AE · Not now | 6 · 4 · 1 · 3 | 1 to Team · 1 restored · 1 Quote open · 0 | on `expansion-arr` and `reactivated-arr`, not here · $2,400 open |
| **Agent half** · your half | **92** · 92 assigned | **11** of 64 matured · 5 of 64 | **$3,817** · $1,735 |
| At 20 a month (`holdout: 0`) | about 37 in 56 days | basis before-after: Before · Now, not proof | Other Startups’ results: +14.0 per 100, ■■■□ Likely, n 3,410 |

| Day | Event | What happened | Your time |
| --- | --- | --- | --- |
| 0 | `event_1121` Campaign.created | the three steps; the dry run fails `limit-on-offer` | 12 min |
| 0 | `event_1124` Offer.published | Leaseline Free gains a 6-read monthly Limit | 10 min |
| 3 | `event_1170` Offer.subscribed | the first upgrade paid: Oakfield, free to Pro | — |
| 9 | `event_1233` Role.updated | promoted: prompts send after a 2-minute hold | — |
| 14 · 44 | `event_1262` Change.merged · `event_1488` Readout.created | the grant from 6 to 10 reads: replayed at 48.1 per 100, read back at +30d at 50.0, n 82 | 5 min |
| 36 | `event_1402` Campaign.paused | `pool` hit $50 on Sep 29; new visitors saw the trial until Oct 1 | 4 min |
| 1–56 | `event_1140` Approval.created, first of 20 | you approved the first wave; `price` refused 1 draft, rewritten once and sent; weekly reviews, from the <Op verb="Task.next" /> log | 22 min · **total 1.6 h measured, 1.7 h asked** |

> **Oakfield Lettings, in Leaseline, Oct 19 10:12** · the 10th lease read refused, inside the renewal notice they were drafting.
>
> **The prompt, in the task** · You’ve used 10 of 10 lease reads this month; they reset Nov 1.¹ Pro is $29 a month for 50 units and 200 reads.² Upgrade and this notice finishes now; switching back sits on your billing page.³
>
> **Oakfield, 10:14** · pressed Upgrade to Pro. `Offer.subscribed`, +$347 ARR.

```diff title="Juniper Homes, 2026-09-18 · the draft price refused, then the rewrite that sent"
- Upgrade to Team before Friday and it’s $59 a month.
+ Team is $79 a month and adds the owner report; today’s charge is $41.20, and switching back sits on the same page.
```

> **Northgate Residential, 2026-10-06 16:20** · “We have 220 units. Is there an annual contract?” · **Leaseline, 16:21** · *Thanks. Priya, your account lead, replies by tomorrow 16:20 with the annual plan.*⁴ The Brief went to Priya (`task_441`).

¹ The true units left and the reset time, from the Entitlement. ² The next Offer’s Price, from the Pricing Page. ³ The switch down, one press away. ⁴ The routed line, compiled from the Role’s Settings.

## Your settings
```mdx title="plays/product-led.mdx"
---
holdout: 0.5      # half the free signups get today’s free plan and upgrade page
---
Give free accounts 10 lease reads a month, 2 a day: twice what activation takes.
In the first 30 paid days, offer Team when a second member is refused the owner report.
Over 150 units, bring in our AE.
```

```text
$ npx startup-builder show plays/product-led
holdout 0.5 · grant 10 reads · Team on a refused member · AE over 150 units     your file  plays/product-led.mdx:2,4,5,6 (Ask 2026-08-24; line 4 by change_19, 2026-09-07)
worker role:success-manager, owns never · chooses 6 (3 rule, 3 read) · sequences bound-follow-up, trial-to-paid, first-30-upsell, win-back-1-30-60 · approval first-send · checks owned-by-a-person, keep-first, customer-ask-cap, price, exit-never-waits, abuse · stops 8 (5 on in-product and email) · placements gmail, outlook, yahoo-mail, app-store, google-play, chrome-web-store, github, google-search    catalog  play:product-led@1 (Not live yet, gaps #5)
```

### Every key from the catalog
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-meter-line"></a>`meter-line` | Meter line frees cheap Verbs, meters the value Verb through a daily Credit Grant, and caps it at twice the activation path. Starts at You approve each one. |
| Tactic | <a id="tactic-free-cost-pool"></a>`free-cost-pool` | Free Offer cost pool books the serving cost of free and trialing Customers to a marketing Account under a monthly pool, and stops free-use abuse. Starts at Sends nothing. |
| Tactic | <a id="tactic-entry-variants"></a>`entry-variants` | Entry Variants run entry terms as parallel Offers at one Price, split by a Flag, and decide each on day-60 revenue per 1,000 Visitors. Starts at You approve the first wave. |
| Tactic | <a id="tactic-usage-prompt"></a>`usage-prompt` | Usage prompt offers the upgrade inside the task that meets a usage bound or the paid-predictor pattern, with the true limit and Price. Starts at You approve the first wave. |
| Tactic | <a id="tactic-first-30-upsell"></a>`first-30-upsell` | First 30-day upsell spends its touches in a new paying Customer’s first 30 days, from their own usage, scored at day 90 against a holdout. Starts at You approve the first wave. |
| Tactic | <a id="tactic-win-back-1-30-60"></a>`win-back-1-30-60` | Win-back at days 1, 30 and 60 makes cancel one step, then sends a restore, the matched fixes and a fitting Offer, never a discount. 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. |
| Trigger | <a id="trigger-usage-threshold"></a>`usage-threshold` | Usage threshold: `threshold` on `Entitlement.checked`, window 2d. |
| Trigger | <a id="trigger-paid-predictor"></a>`paid-predictor` | Paid predictor: `threshold` on `Event.captured`, window 7d. |
| Trigger | <a id="trigger-activation-reached"></a>`activation-reached` | Activation reached: `event` on `Customer.activated`, window 72h. |
| Trigger | <a id="trigger-first-paid"></a>`first-paid` | First paid: `event` on `Offer.subscribed`, window 30d. |
| Trigger | <a id="trigger-limit-pressure"></a>`limit-pressure` | Limit pressure: `threshold` on `Entitlement.checked`, window 3d. |
| Trigger | <a id="trigger-subscription-ended"></a>`subscription-ended` | Subscription ended: `state` on `Subscription.ended`, window 60d. |
| Trigger | <a id="trigger-reason-fixed"></a>`reason-fixed` | Reason fixed: `event` on `Version.deployed`, window 30d. |
| Trigger | <a id="trigger-verb-cost-shift"></a>`verb-cost-shift` | Verb cost shift: `threshold` on `Metric.watched`, window 7d. |
| Trigger | <a id="trigger-pool-threshold"></a>`pool-threshold` | Pool threshold: `threshold` on `Metric.watched`, window 1d. |
| Sequence | <a id="sequence-bound-follow-up"></a>`bound-follow-up` | Bound follow-up: 1 steps; it ends on Upgrade; Single-task purchase; Unsubscribe; The grant resets. |
| Sequence | <a id="sequence-trial-to-paid"></a>`trial-to-paid` | Trial to paid: 3 steps; it ends on Paid; Cancel; Unsubscribe. |
| Sequence | <a id="sequence-first-30-upsell"></a>`first-30-upsell` | First 30-day upsell: 2 steps; it ends on Switch; Downgrade; Refund request; Failed charge; Negative support Issue; Unsubscribe; Day 30. |
| Sequence | <a id="sequence-win-back-1-30-60"></a>`win-back-1-30-60` | Win-back at days 1, 30 and 60: 4 steps; it ends on Restore; Unsubscribe; A no-contact reason code; A fix Message sent after day 30, which replaces the day-60 touch; Day 60. |
| Benchmark | <a id="benchmark-product-led-month-1-upgrade"></a>`product-led-month-1-upgrade` | Monthly upgrade rate in a Subscription's first month, upgrades to a higher Offer (low) and to more units (high): 0.75–0.83 % (A subscription-analytics vendor's purchase data, published by its affiliated analyst, 2026-09-30). |
| Benchmark | <a id="benchmark-product-led-month-1-multiple"></a>`product-led-month-1-multiple` | Month-1 upgrade rate as a multiple of the month-4 rate, to a higher Offer and to more units: 4.4–4.7 ratio (A subscription-analytics vendor's purchase data, published by its affiliated analyst, 2026-09-30). |
| Benchmark | <a id="benchmark-product-led-renewal-upgrade-share"></a>`product-led-renewal-upgrade-share` | Share of first-year upgrades to a higher Offer made at the month-12 renewal: 40 % (A subscription-analytics vendor's purchase data, published by its affiliated analyst, 2026-09-30). |
| Benchmark | <a id="benchmark-product-led-churn-return"></a>`product-led-churn-return` | Share of churned subscribers who return within 3 months, all products (low) and AI-native products (high): 5–10 % (A subscription-analytics vendor's purchase data, published by its affiliated analyst, 2026-09-30). |
| Benchmark | <a id="benchmark-product-led-card-trial"></a>`product-led-card-trial` | Free-to-paid conversion within six months of free trials that require a card: 30 % (A growth analyst's free-to-paid survey with two subscription-analytics vendors; correlational, 2026-02-04). |
| Benchmark | <a id="benchmark-product-led-hard-paywall-d35"></a>`product-led-hard-paywall-d35` | Median day-35 conversion (trial-to-paid in the text, download-to-paid in the chart), freemium apps (low) against hard onboarding paywall apps (high): 2.1–10.7 % (A mobile subscription-billing vendor's annual app report; correlational, 2026-03-19). |
| Benchmark | <a id="benchmark-product-led-rpi-d60"></a>`product-led-rpi-d60` | Median revenue per install at day 60, freemium apps (low) against hard onboarding paywall apps (high): $0.38–$3.09 (A mobile subscription-billing vendor's annual app report; correlational, 2026-03-19). |
| Benchmark | <a id="benchmark-product-led-trial-length"></a>`product-led-trial-length` | Median trial-to-paid, trials under 4 days (low) against 17 to 32 days (high): 25.5–42.5 % (A mobile subscription-billing vendor's annual app report; correlational, 2026-03-19). |
| Benchmark | <a id="benchmark-product-led-day-0-cancel"></a>`product-led-day-0-cancel` | Share of 3-day-trial cancellations made on day 0: 55.4 % (A mobile subscription-billing vendor's annual app report, 2026-03-19). |