Objection watch
Records why each Customer or Deal said no, holds them from other outreach, and sends one Message, with proof, once the reason stops being true.
- Step
- Keep Customers
- Channel
- What they tell you
- Kind
- Outreach
Be our CSM for every no: when a Deal is lost or a Customer cancels, record why in their words and keep everyone else from pitching them. When that reason stops being true, send one note with the proof. Big accounts are mine. Show me what’s missing; don’t send.
# Not live yet (gaps #2)
curl -X POST "https://api.sb/leaseline.app/campaigns?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"play":"objection-watch"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'objection-watch' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-06-01 · Not live yet, gaps #2
npx startup-builder do Campaign.create --json '{"play":"objection-watch"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'objection-watch' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-06-01 · Not live yet, gaps #2",
"mode": "try"
}
}application/json{
"type": "https: //startupbuilder.dev/ errors/ refused",
"title": "Refused",
"status": 422,
"detail": "objection-watch can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "26 nos a month: lost Deals, cancels, downgrades and stalled trials",
"doc": "/plays/ objection-watch#check-use-when",
"fix": "Nothing to do: every no with a reason counts.",
"then": "Metric.query"
},
…
Hand our latest objection whose condition came true 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(%22Deal.lost%22%2C%20%22Subscription.unsubscribed%22%2C%20%22Subscription.switched%22%2C%20%22Subscription.lapsed%22%2C%20%22Conversation.labeled%22%2C%20%22Consent.granted%22%2C%20%22Issue.resolved%22%2C%20%22Version.deployed%22%2C%20%22Offer.published%22%2C%20%22Organization.repriced%22%2C%20%22Organization.deprecated%22%2C%20%22Company.funded%22%2C%20%22Opening.published%22%2C%20%22Organization.appointed%22%2C%20%22Organization.announced%22%2C%20%22Objection.dated%22%2C%20%22Conversation.answered%22%2C%20%22Content.published%22%2C%20%22Change.reverted%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_1290"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22customer_38%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_38%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Deal.lost", "Subscription.unsubscribed", "Subscription.switched", "Subscription.lapsed", "Conversation.labeled", "Consent.granted", "Issue.resolved", "Version.deployed", "Offer.published", "Organization.repriced", "Organization.deprecated", "Company.funded", "Opening.published", "Organization.appointed", "Organization.announced", "Objection.dated", "Conversation.answered", "Content.published", "Change.reverted")', 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 (\"Deal.lost\", \"Subscription.unsubscribed\", \"Subscription.switched\", \"Subscription.lapsed\", \"Conversation.labeled\", \"Consent.granted\", \"Issue.resolved\", \"Version.deployed\", \"Offer.published\", \"Organization.repriced\", \"Organization.deprecated\", \"Company.funded\", \"Opening.published\", \"Organization.appointed\", \"Organization.announced\", \"Objection.dated\", \"Conversation.answered\", \"Content.published\", \"Change.reverted\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1290"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"customer_38\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"customer_38\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Deal.lost\", \"Subscription.unsubscribed\", \"Subscription.switched\", \"Subscription.lapsed\", \"Conversation.labeled\", \"Consent.granted\", \"Issue.resolved\", \"Version.deployed\", \"Offer.published\", \"Organization.repriced\", \"Organization.deprecated\", \"Company.funded\", \"Opening.published\", \"Organization.appointed\", \"Organization.announced\", \"Objection.dated\", \"Conversation.answered\", \"Content.published\", \"Change.reverted\")', limit: 1 }) // { play } compiled to this Play’s Trigger Events\nconst { subject } = await $.Trigger.fire({ like: last.$id }, { mode: 'try' }) // the same Event again, in try mode · Not live yet, gaps #7\nconst { decisions: [decision] } = await $.Decision.list({ where: `on = \"${subject}\"` }) // what it chose, and why · Not live yet, gaps #7\nconst { actions } = await $.Action.list({ where: `on = \"${subject}\"` }) // what it did, with its checks · Not live yet, gaps #7\nreturn { decision, actions }",
"mode": "try"
}
}application/json{
"decision": {
"$id": "https: //api.sb/ leaseline.app/ decisions/ decision_502",
"$type": "Decision",
"choice": "send-the-one",
"reason": "Oakridge Flats cancelled Jun 3 saying “owner statements only come out as CSV”; issue_77 shipped statements in the owner portal on Oct 16 and has held all traffic for 3 days, so the proof rule passes",
"stakes": "low",
"options": [
"send-the-one",
"you-write-it",
"name-what-it-answers",
"send-the-correction",
"keep-holding"
],
…
sample · 4 Not live yetPreviewOn PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim
action_3 is the one Message to contact_14, in the thread of Oakridge’s cancel, held 30 minutes so you can stop it. The proof is read again just before it sends.
$694 objection dollars closed against $0 before · before-after,
| Try another: the holder | Choice | Reason |
|---|---|---|
| Oakridge Flats: cancelled Jun 3, “owner statements only come out as CSV” | send-the-one | issue_77 shipped portal statements and held all traffic 3 days |
| Birchway Properties: lost Deal, “we need it to work for 600 units”, a top-tier holder | you-write-it | top tier: the one Message is yours to write |
| Kestrel Rentals: downgraded, “too slow on mobile, and no bulk edits” | name-what-it-answers | the mobile fix shipped; bulk edits have not |
| Alder & Co: lost Deal, “no e-signing”; the e-sign Change was reverted Oct 11 | send-the-correction | a fired set’s Change came out: one correction within 24 h |
- 1 h week 1, then 15 min a week
- Your time
- Day 52: first former Customer back
- First signal
- 20+ nos a month, each recorded with a reason
- Use it when
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Objection dollars closed → Won back ↑ → ARR added each week → ARR
Worth’s formula: about +9 returns per 100 former-Customer sets fired (Other Startups’ results; no lift of your own yet) × 3 sets a month × $347 Pro ARR ≈ +$94; the Prospects’ sets read beside it as Deals opened. Under First signal: At 3 former-Customer sets a month, 90 days can tell only +40 per 100 or more, so your file reads before-after; a set matures 90 days after it fires.
sample · api.sb/
Start it this week
npx startup-builder add play objection-watch # 1 · writes plays/objection-watch.mdx (Not live yet, gaps #5)
# 2 · dry run; below, its 422 on 2026-06-01
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":"objection-watch"}'
# 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":"objection-watch"}'
# on
curl -X PATCH https://api.sb/leaseline.app/campaigns/objection-watch \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":null}'
# off: no fire sends; every hold stays in force
curl -X PATCH https://api.sb/leaseline.app/campaigns/objection-watch \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endsAt":"now"}' | Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | Nothing to do: every no with a reason counts. | POST /{namespace}/metrics/query |
billing-connected fail · Not live yet, gaps #8 | Connect Stripe so every cancel and downgrade arrives with its reason code. | POST /{namespace}/integrations/connect |
baseline warn · Not live yet, gaps #8 | n 9: it reads before-after, not proof, until volume allows a held-back half. | POST /{namespace}/metrics/query |
owned-by-a-person pass · Not live yet, gaps #8 | Nothing to do: their fires draft into your queue instead. | POST /{namespace}/tasks/{task}/assign |
keep-first pass · Not live yet, gaps #8 | Nothing to do: a Customer at risk or past due hears from their CSM first. | POST /{namespace}/tasks/next |
customer-ask-cap pass · Not live yet, gaps #3 | Nothing to do: one open ask per Customer, across every Play. | GET /{namespace}/decisions |
role-unstaffed warn · Not live yet, gaps #8 | Staff the CSM and AE, or leave them: replies and big accounts come to you. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "objection-watch can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "26 nos a month: lost Deals, cancels, downgrades and stalled trials",
"doc": "/plays/objection-watch#check-use-when",
"fix": "Nothing to do: every no with a reason counts.",
"then": "Metric.query"
},
{
"check": "billing-connected",
"status": "fail",
"doc": "/plays/objection-watch#check-billing-connected",
"fix": "Connect Stripe so every cancel and downgrade arrives with its reason code.",
"then": "Integration.connect"
},
{
"check": "baseline",
"status": "warn",
"observed": "0 of 9 former Customers back in 90 days, $0",
"doc": "/plays/objection-watch#check-baseline",
"fix": "n 9: it reads before-after, not proof, until volume allows a held-back half.",
"then": "Metric.query"
},
{
"check": "owned-by-a-person",
"status": "pass",
"observed": "3 holders have a person owner, you",
"doc": "/plays/objection-watch#check-owned-by-a-person",
"fix": "Nothing to do: their fires draft into your queue instead.",
"then": "Task.assign"
},
{
"check": "keep-first",
"status": "pass",
"observed": "0 held",
"doc": "/plays/objection-watch#check-keep-first",
"fix": "Nothing to do: a Customer at risk or past due hears from their CSM first.",
"then": "Task.next"
},
{
"check": "customer-ask-cap",
"status": "pass",
"observed": "0 open asks",
"doc": "/plays/objection-watch#check-customer-ask-cap",
"fix": "Nothing to do: one open ask per Customer, across every Play.",
"then": "Decision.list"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"success-manager",
"sales-lead"
],
"owned": 3,
"doc": "/plays/objection-watch#check-role-unstaffed",
"fix": "Staff the CSM and AE, or leave them: replies and big accounts come to you.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
use-when · pass: 26 nos a month (gaps #8) | POST /{namespace}/metrics/query on objection-dollars-closed | 1 min | none |
billing-connected · fail | POST /{namespace}/integrations/connect with system recurring-billing (Stripe) | 5 min | the first sync, about 10 min |
baseline · warn: 0 of 9 in 90 days | POST /{namespace}/metrics/query; it fills Before in Your number | 0 min | none |
owned-by-a-person · keep-first · customer-ask-cap · pass: 3 owned by you; 0 held; 0 open asks (gaps #3, #8) | nothing; each runs again on every fire | 0 min | none |
role-unstaffed · warn: 3 holders have a person owner, you | POST /{namespace}/roles/{role}/staff, or leave it: replies and top-tier sets come to you | 0 min | none |
Your number · Before $0 · Now $694 objection dollars closed · Target $1,
Capturing and coding a no costs about $0.01, and each fire about $0.04 from your Domain (Worker cost). The condition checks read your own Issues and public pages at no cost. That comes to about $1 a month at 25 nos and 5 fires.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A Customer cancels, with a reason | subscription-ending | event on Subscription.unsubscribed where reason != null | captured within 1 h | POST /{namespace}/integrations/connect (Stripe); your cancel form posts the reason |
| A Deal is lost, with the code and their own words | deal-lost | event on Deal.lost where lossReason != null | captured within 1 h | POST /{namespace}/deals/{deal}/lose with reason |
| An Issue linked to open objections ships and holds all traffic | issue-resolved | event on Issue.resolved where linkedObjections > 0 and heldAllTraffic = true | 30d | POST /{namespace}/issues/{issue}/resolve |
+ 10 more: downgrades, stalled trials and not-now replies that open a set; and rival pages, new buyers, dated conditions and trust items that can close one (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1290" ,
"$type": "Event",
"name": "Issue.resolved",
"at": "2026-10-19T06:00:11Z" ,
"seq": 1290,
"data": {
"issue": {
"$id": "https://api.sb/leaseline.app/issues/issue_77" ,
"title": "Owner statements in the owner portal",
"resolvedAt": "2026-10-16",
"heldAllTraffic": true
},
"linkedObjections": [
"objection_12",
"objection_19"
],
"openDollars": 694
}
}npx startup-builder trigger Issue.resolved --like event_1290 # replays the delivery in test
npx startup-builder clock advance --by 30m # sends the one Message (action_3) after its hold
npx startup-builder listen Subscription.created --forward-to http://localhost:3000/hook Who runs it
CSM · your agent (role:, worker_5) holds the Key Result. A reply that wants to come back goes to AE · Priya Shah (person) (worker_3, staffed on day 9); top-tier sets and every set whose proof cites a named rival come to you (Dana Okafor, person, worker_1), because the CSM Role has no person. Your number links to Did it work.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Send the one Message | It does it, you can stop it | 11 of 11 approved unedited | met on day 71 (the first 10 fires, none not-answered) | a not-answered reply, or the double-fire Stop → You approve each one | POST /{namespace}/actions/{action}/cancel inside the 30-minute hold |
| Name what it answers · Keep holding | It does it, you can stop it | 3 of 3 · 41 of 41 | the same | the same | the same |
| You write it · Send the correction | You approve each one | 4 of 4 · 1 of 1 | never: top-tier sets and corrections stay yours | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| Change a proof rule | You approve each one | 1 merged of 1 | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a second Message on one set, a discount, a shipped date for something unshipped | Refused, every time | 0 sent | never | — | none: the Stop holds it |
What it reads · What it may call. It reads the holder’s own words, the Issue and the proof source, and never a Visit, an open or a return to the site. A held holder is held for every Play: other Plays’ pool steps refuse a promotional touch until the set closes. Its tools are POST /{namespace}/messages/{message}/schedule, POST /{namespace}/tasks/{task}/assign, POST /{namespace}/content/draft, and any other call becomes an Approval under the Role’s Mandate.
You hold the CSM Key Result: $1,500 objection dollars closed by 2026-12-31. ← worker, metric
On each condition that comes true, choose one option and say why. ← triggers, chooses
Quote their words with their date; never make them stronger. ← copyRules 1
Cite the proof with its date, or do not send. ← copyRules 3
Name only what the proof answers; no date for anything unshipped. ← copyRules 6–7
Say this is the only Message about it, with one ask. ← copyRules 10–11 The slow loop, as a Change. decision_288 (2026-08-17, weekly) read 4 mobile-speed sets whose proof was the release note alone, with 2 not-answered replies. change_19 made the proof rule read the measured page time as well (your file, line 7), and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on past fires, POST /{namespace}/changes/{change}/review held 2 of the 4 back; read back at +30d, 0 not-answered of 3 fires, n 3, ■□□□ Too early to tell (Example, day 108).
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 each one when it does not; POST /{namespace}/roles/{role}/staff seats it.
npx startup-builder eval --scenario objection-watch-118 --agent "claude -p" --seat success-manager # seated: 21 of 21 sets falsified correctly, 0 double fires, $0.04 a fire · yours: 19 of 21 → You approve each one What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Send the one Message send-the-one | the proof rule passes and answers the whole objection | Your rule (line 5) | objection-fire-one in the thread of their no → POST /{namespace}/messages/{message}/schedule, then a 30-day reply window | a reply that wants to come back: POST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 1 business day · Take this one | you |
You write it you-write-it | a top-tier holder, or the proof cites a named rival | Your rule (line 6) | objection-fire-founder → POST /{namespace}/tasks/{task}/assign to you with the set and the proof | you write and send it · Take this one | you |
Name what it answers name-what-it-answers | the proof answers part of the objection | Agent’s call, with its reason | objection-fire-one, naming only what shipped → POST /{namespace}/content/draft | the same as Send the one Message | you |
Send the correction send-the-correction | a Change behind a fired set was reverted | Your rule (the Revert stop) | objection-correction in the same thread, within 24 h | you approve it · Take this one | you |
Keep holding keep-holding | the proof fails or is stale, the set is held back, or keep-first applies | Agent’s call, with its reason | nothing; the hold stays, and names what it waits on | the hold, until the condition is true or it expires | the same |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | “Good timing. Can we restart on 160 units?” | over 150 units: a Quote; under it, the Pro restart link | AE · Priya Shah (person) | 1 business day | POST /{namespace}/quotes/{quote}/send |
| Send me pricing | “What would it cost us now?” | the Pricing Page’s list Price, no private Price | CSM · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Maybe after our January budget.” | the set closes; January is recorded as a new condition date | nobody | at once | GET /{namespace}/decisions |
| Wrong person | “Our new ops lead, Sam, owns tools now.” | asks them to bring Sam in; it never writes to an address from their text | CSM · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply until Oct 28 | the 30-day reply window runs on; nothing else sends | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Do statements include the owner’s tax forms?” | To a person, the question in the Brief | AE · Priya Shah, or you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “Please don’t contact us again.” | nothing else sends from any Play, and the set closes | nobody | at once | POST /{namespace}/contacts/{contact}/suppress |
When it hands to you
Replies that want to come back go to AE · Priya Shah (person); top-tier sets and sets citing a rival come to you, and before day 9 every reply did. One Brief per Role, as captured (brief is Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_391" ,
"$type": "Task",
"assignee": "worker_3",
"due": "2026-09-23T15:40:00Z" ,
"on": "customer_29",
"brief": {
"who": "Sable Street Homes, cancelled May 19" ,
"said": "Good timing. Can we restart on 160 units?" ,
"objection": "no bulk lease edits",
"proof": "issue_64, shipped Sep 15" ,
"chose": "send-the-one",
"why": "the proof passed; the whole objection is answered" ,
"never": [
"a discount",
"a second Message on this set"
]
}
},
{
"$id": "https://api.sb/leaseline.app/tasks/task_342" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-09-04T17:00:00Z" ,
"on": "lead_47",
"brief": {
"who": "Birchway Properties, lost Deal Jul 2" ,
"said": "We need it to work for 600 units.",
"chose": "you-write-it",
"why": "a top-tier holder",
"proof": "the 600-unit load test, published Sep 1"
}
}
]The 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, each a one-call window. Once a person claims, the agent sends nothing more on that set, and every reply goes to the claimer (claimedByPerson, Not live yet, gaps #7). A Task nobody claims by its due time goes up a Role through POST /{namespace}/tasks/{task}/escalate, and person-promise fires.
On time: AE · Priya Shah, 3 of 3 claimed within 1 business day, median 2 h 05 min, n 3; you, 4 of 4 sent within 3 days, n 4, ■□□□ Too early to tell beside Other Startups’ 91 % on time. Your queue: counted by kind: in week 1, 18 labels to confirm and 0 fires; after week 3, about 3 labels and 1 fire a week, plus the Monday Issue ranking.
The fixed line: POST /{namespace}/deals/{deal}/assign sets owner to the receiving person, the agent is never a Deal’s owner, and Meetings land on the owner’s calendar under the owner’s name.
When it won’t send
| Check (refuses one send) | Refuses | Fix | Then |
|---|---|---|---|
owned-by-a-person (rule j, gaps #8) | a fire to a holder 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. | POST /{namespace}/tasks/{task}/assign |
keep-first (rule j, gaps #8) | a fire to a Customer who is at risk, past due or has an unsubscribe pending | Let their CSM reach them first, then ask again once they are healthy. | POST /{namespace}/tasks/next |
customer-ask-cap (gaps #3) | a second open ask to one Customer in a window, from any Play | next-outcome holds them until Nov 18. | GET /{namespace}/decisions |
proof-drift (the record’s Proof drift stop) | a fire whose proof source changed or vanished since it was captured | Re-read the proof; the fire sends only if it passes again. | GET /{namespace}/issues/{id} |
label-backlog (the record’s Label backlog hold) | a fire on a set whose label nobody confirmed | Confirm the label on the lost Deal, then the set can fire. | PATCH /{namespace}/deals/{id} |
market-hold (the record’s Market hold) | a fire to a Market that is unresolved or failed its row check | Resolve the holder’s Market; the fire never retries another way. | PATCH /{namespace}/organizations/{id} |
fact-check (Not live yet) | a line the proof, the claims source or the holder’s own record does not back | Cut the claim, or name the record that backs it. | POST /{namespace}/content/draft |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that sent is the Kestrel thread in Example.
| Check | Fix | Then |
|---|---|---|
fact-check fail · Not live yet, gaps #1 | Cut the claim, or name the record that backs it. | POST /{namespace}/content/draft |
proof-drift pass | — | GET /{namespace}/issues/{id} |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The fire to Kestrel Rentals did not send: it says both of their reasons are fixed, and the proof shows one.",
"checks": [
{
"check": "fact-check",
"status": "fail",
"doc": "/plays/objection-watch#check-fact-check",
"fix": "Cut the claim, or name the record that backs it.",
"then": "Content.draft"
},
{
"check": "proof-drift",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "hold-breach",
"observed": "1 promotional touch from lead-nurture to a held holder",
"bound": "0",
"window": "30d"
},
"doc": "/plays/objection-watch#stop-hold-breach" ,
"task": "task_377"
}
]Did it work
At 3 former-Customer sets a month, no held-back half reads within 90 days, so the basis is before-after (rule o): the columns read Now and Before, and nothing prints as Added. Only sets 90 days past their fire count (rule n). Each figure’s call is POST /{namespace}/metrics/query by node, and every node above the leaf is Not live yet (gaps #4).
$694 objection dollars closed against $0 before · before-after,
- Chose
- send-the-one
- Why
- Oakridge Flats cancelled Jun 3 saying “owner statements only come out as CSV”; issue_77 shipped statements in the owner portal on Oct 16 and has held all traffic for 3 days, so the proof rule passes
- From
- send-the-one · you-write-it · name-what-it-answers · send-the-correction · keep-holding
- Expects
- Subscription.created by customer_38 within 90d of the fire
- Reopens when
- no inbound Message.created from contact_14 within 30d: the set closes and nothing else sends
- By
- worker_5
JSON
{
"$id": "decisions/decision_502",
"$type": "Decision",
"choice": "send-the-one",
"reason": "Oakridge Flats cancelled Jun 3 saying “owner statements only come out as CSV”; issue_77 shipped statements in the owner portal on Oct 16 and has held all traffic for 3 days, so the proof rule passes",
"stakes": "low",
"options": [
"send-the-one",
"you-write-it",
"name-what-it-answers",
"send-the-correction",
"keep-holding"
],
"by": "worker_5",
"on": "customer_38",
"expected": "Subscription.created by customer_38 within 90d of the fire",
"reopensWhen": "no inbound Message.created from contact_14 within 30d: the set closes and nothing else sends"
}| Node | Now (140 days) | Before (the 90 days, per 140) | Added | Confidence | Basis |
|---|---|---|---|---|---|
objection-dollars-closed | $694: 2 of 3 matured former-Customer sets back on Pro · 2 still maturing · Booked $694: credited, not all caused by this Play | $0: 0 of 9 cancels back | — · Worth about +$100 a month | — | before-after, not proof · Other Startups’ results +9 returns per 100, ■■■□ Likely, n 1,460 |
reactivated-arr ↑ | $1,041 | $347 | — | — | before-after; includes 1 return this Play never fired on |
net-new-arr · arr | carried from reactivated-arr | carried | — | — | carried |
Before against after reads former Customers who came back in this window against the 90 days before it, scaled to 140 days; it is not proof, and Other Startups’ results print beside it. Beside the path, the Prospects’ 14 fired sets opened 4 Deals and won 1 on the $2,400 plan (+$2,393 ARR), read as opened-deals-and-trials.
How it moves it: each holder heard once, a median 38 minutes after the proof passed, in the thread of their own no, where before nobody wrote back at all (objection-dollars-closed by fire latency).
Guardrails: before against after: contraction-arr, $0 from returners who switched down, against $0, and hold breaches, 1 in 140 days, against a Stop at 2 in 30.
{
"$id": "https://api.sb/leaseline.app/campaigns/objection-watch" ,
"$type": "Campaign",
"play": "play:objection-watch@1" ,
"path": [
"objection-dollars-closed",
"reactivated-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "objection-dollars-closed",
"unit": "usd",
"heldBack": 2,
"decision": null
},
"readout": {
"value": null,
"n": 3,
"maturing": 2,
"interval": null,
"readability": "inadmissible",
"basis": "before-after",
"pooled": true
},
"ledger": {
"spend": 2,
"workerCost": 1,
"hours": 4.6,
"pipelineArr": 4786,
"wonArr": 3087,
"projectedArr": 94,
"paybackDays": 4
}
}Outside benchmark: 5 % to 10 % of churned subscribers come back on their own (a subscription-analytics vendor’s billing data, 2026-09-30; a base rate, not a lift).
Before it · After it. Before it: every lost Deal from instant-response and cold-email, every cancel from usage-upsell’s Customers and every stalled trial opens a set here. After it: a reply that wants to come back reaches the AE, Not now feeds lead-nurture with a dated condition, and new condition dates feed moment-outreach. While a set stands, every other Play’s pool step holds the holder.
Example
Leaseline ran it from 2026-06-01 to 2026-10-19 (140 days): 118 nos captured, 15 held by former Customers and 103 by Prospects. 21 sets came true, 2 were held back at random, and 19 fired: 5 former Customers’ and 14 Prospects’. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Sets | Replies | Came back or opened a Deal | Won (credited, not all caused by this Play) |
|---|---|---|---|---|
| Send the one Message | 11 | 5 | 2 former Customers back, 3 Deals | 2 on Pro (+$694 ARR) |
| You write it | 4 | 3 | 1 Deal | 1 on the $2,400 plan (+$2,393 ARR) |
| Name what it answers · Send the correction · Keep holding | 3 · 1 · 41 | 1 · 0 · 0 | 0 | 0 |
| All fires · held back | 19 · 2 | 9 · — | 2 back, 4 Deals · 0 | $3,087 · $0 |
| At 20 nos a month (the Use it when volume) | about 13 fires in 140 days | before-after, not proof | — | Other Startups’ results: +9 returns per 100, ■■■□ Likely, n 1,460 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_101 Campaign.created | the three steps; the dry run fails billing-connected | 20 min |
| 0–21 | event_133 Objection.captured, first of 118 | you confirmed 31 labels in the first 3 weeks; every set held its holder | 1 h 50 min |
| 9 | event_190 Role.staffed | AE · Priya Shah (person) takes role: | — |
| 52 | event_611 Subscription.created | the first former Customer back: Sable Street Homes, on Pro | — |
| 71 | event_788 Role.updated | promoted: fires send after a 30-minute hold; top-tier sets stay yours | — |
| 78 · 108 | event_802 Change.merged · event_1004 Readout.created | the mobile proof rule reads page time: read back 0 not-answered of 3 | 5 min |
| 96 | event_915 Campaign.paused | hold-breach: lead-nurture touched a held holder; its pool step was fixed the same day | 15 min |
| 1–140 | event_140 Approval.created, first of 19 | you approved fires and wrote 4; Monday Issue rankings, from the POST /{namespace}/tasks/next log | 2 h 30 min · total 4.6 h measured, 4 h asked |
Leaseline → Sable Street Homes, Sep 22, 09:12, in the thread of their cancel · On May 19 you wrote: “We cancelled because there are no bulk lease edits.”¹ Bulk edits shipped on Sep 15 and have run on every account since (release note, Sep 15).² This is the only note about it, and nothing else follows unless you reply.³ Want the restart link? *Signed with the Role’s disclosure line and the stop line.*⁴
Sable Street, Sep 22, 14:40 · “Good timing. Can we restart on 160 units?” The Brief reached Priya at 14:41; she claimed it at 15:02 and sent the Quote the next morning (When it hands to you).
- Good news: mobile is fast now and bulk edits are live, so everything you asked for is fixed.
+ On Jul 2 you named mobile speed and bulk edits. Mobile pages now load in 1.2 s (measured Sep 7); bulk edits have not shipped. Birchway Properties, 2026-09-03 · set held since a lost Deal on Jul 2: “We need it to work for 600 units.” · the 600-unit load test published Sep 1 passed the proof rule, and the set went to Dana as a top-tier holder (
task_342). Dana wrote and sent the one Message on Sep 4 at 11:20, inside the promised time; Birchway booked a call that afternoon.
¹ Their own words, verbatim, with their date. ² The proof, with its date. ³ The only Message about this objection. ⁴ The fixed signature and stop lines, compiled from the Role’s Settings.
Your settings
---
holdout: 0.1 # 1 set in 10 that comes true gets no Message for 90 days
---
Record every no in their own words, and hold them from every other Play.
One Message, with proof, when the reason stops being true.
Accounts over 300 units are mine to write.
A speed fix counts only once the measured page time shows it.$ npx startup-builder show plays/objection-watch
holdout 0.1 · top tier over 300 units · proof reads page time your file plays/objection-watch.mdx:2,4,5,6,7 (Ask 2026-06-01; line 7 by change_19, 2026-08-17)
family keep · worker role:success-manager, owns never · chooses 5 (3 rule, 2 read) · sequences objection-fire-one, objection-fire-founder, objection-correction · approval delay-window · checks owned-by-a-person, keep-first, customer-ask-cap, proof-drift, label-backlog, market-hold, fact-check · stops 8 · placements gmail, outlook + the founder’s LinkedIn tray catalog play:objection-watch@1 (Not live yet, gaps #5) Every key from the catalog
Compiled, not written: edit the Play record objection-watch 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.