What they tell you: reasons, coded each week
What they tell you: every cancel reason coded each week, one interview, and a note when it is fixed.
Every cancel reason is coded each week, and the Customers who gave it hear back when it is fixed.
You, to your agent
Why did customers leave this quarter, and who do we talk to first?
curl "https://api.sb/leaseline.app/events?where=name%20%3D%20%22Subscription.unsubscribed%22&limit=20" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
curl -X POST https://api.sb/leaseline.app/collections/query \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"select data.reason, count(*) as customers from events where name = '\''Subscription.unsubscribed'\'' and at >= '\''2026-07-21'\'' group by data.reason"}'
curl -X POST "https://api.sb/leaseline.app/meetings/book?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact":"contact_44","at":"2026-10-22T16:00:00Z"}'
curl -X POST "https://api.sb/leaseline.app/messages/message_99/schedule?mode=try" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"at":"2026-10-26T14:00:00Z"}'import { $ } from 'startup-builder'
const { events } = await $.Event.list({ where: 'name = "Subscription.unsubscribed"', limit: 20 }) // 6 cancels since Jul 21, each with its reason code
const { rows } = await $.Collection.query({ query: 'select data.reason, count(*) as customers from events where name = \'Subscription.unsubscribed\' and at >= \'2026-07-21\' group by data.reason' })
await $.Meeting.book({ contact: 'contact_44', at: '2026-10-22T16: 00: 00Z' }, { mode: 'try' }) // signal-interview: 20 minutes with Aspen Row’s office manager
return await $.Message.schedule({ message: 'message_99', at: '2026-10-26T14: 00: 00Z' }, { mode: 'try' }) // close-the-loop: owner statements shipped, to the 2 who asked
npx startup-builder do Event.list --json '{"where":"name = \"Subscription.unsubscribed\"","limit":20}'
npx startup-builder do Collection.query --json '{"query":"select data.reason, count(*) as customers from events where name = '\''Subscription.unsubscribed'\'' and at >= '\''2026-07-21'\'' group by data.reason"}'
npx startup-builder do Meeting.book --json '{"contact":"contact_44","at":"2026-10-22T16:00:00Z"}' --try
npx startup-builder do Message.schedule --json '{"message":"message_99","at":"2026-10-26T14:00:00Z"}' --try{
"name": "do",
"arguments": {
"code": "const { events } = await $.Event.list({ where: 'name = \"Subscription.unsubscribed\"', limit: 20 }) // 6 cancels since Jul 21, each with its reason code\nconst { rows } = await $.Collection.query({ query: 'select data.reason, count(*) as customers from events where name = \\'Subscription.unsubscribed\\' and at >= \\'2026-07-21\\' group by data.reason' })\nawait $.Meeting.book({ contact: 'contact_44', at: '2026-10-22T16:00:00Z' }, { mode: 'try' }) // signal-interview: 20 minutes with Aspen Row’s office manager\nreturn await $.Message.schedule({ message: 'message_99', at: '2026-10-26T14:00:00Z' }, { mode: 'try' }) // close-the-loop: owner statements shipped, to the 2 who asked",
"mode": "try"
}
}Response200
application/json{
"message": {
"$id": "https: //api.sb/ leaseline.app/ messages/ message_99",
"$type": "Message",
"status": "scheduled",
"mode": "try",
"to": "contact_44",
"subject": "Owner statements are live"
}
}
samplePreviewWhat it does
| What it does | Price per unit | Moves | Who does it | Today |
|---|---|---|---|---|
The customer-research Process codes each POST /{namespace}/subscriptions/{subscription}/unsubscribe reason every Monday; signal-interview books the call and close-the-loop writes back. | $0 per coded reason; $0.04 per note (Worker cost) | Lost Customers, Logo churn | Head of customers | every call live |
Set it up
| Check (dry run on a 0-Customer Startup, gaps #8) | Fix | Then | Your time | Wait |
|---|---|---|---|---|
use-when (Not live yet, gaps #8) · pass: 6 cancels with a reason in 90 days | Run the Play Not yet when names, or wait until the volume is there. | POST /{namespace}/metrics/query | 1 min | none |
role-unstaffed (Not live yet, gaps #8) · warn: Head of customers has no person: interviews come to you | Staff Head of customers, or leave it: interviews come to you. | POST /{namespace}/roles/{role}/staff | 0 min | none |
The records
| Reason | Customers | What answers it |
|---|---|---|
| missing-feature: owner statements | 2 | the Change merged Oct 20; a note to each (Win-back) |
| too-expensive | 2 | the $19 Starter plan, offered once (When someone cancels) |
| switched | 1 | objection-watch holds them until the reason stops being true |
| no-longer-needed | 1 | nothing sends |
{
"$id": "https://api.sb/leaseline.app/events/event_1180" ,
"$type": "Event",
"name": "Subscription.unsubscribed",
"at": "2026-09-02T16:12:00Z" ,
"data": {
"subscription": "subscription_4",
"reason": "missing-feature",
"comment": "We need owner statements."
}
}Calls
| Call | Here | Emits | Status |
|---|---|---|---|
GET /{namespace}/events | 6 cancels since Jul 21, each with its reason code | — | live |
POST /{namespace}/collections/query | Collection.queried | live | |
POST /{namespace}/meetings/book | signal-interview: 20 minutes with Aspen Row’s office manager | Meeting.booked | live |
POST /{namespace}/messages/{message}/schedule | close-the-loop: owner statements shipped, to the 2 who asked | Message.scheduled | live |
Events
| Event | When it fires | What hears it |
|---|---|---|
Subscription.unsubscribed | a cancel, with its reason code | coded every Monday by customer-research |
Meeting.booked | a former Customer says yes to 20 minutes | the interview notes, on the reason |
Refuses when
| Check | Refuses | Fix | Then |
|---|---|---|---|
customer-ask-cap (Not live yet, gaps #3) | a second open ask to one Customer in a window, from any Play | Wait for the open ask to close; GET /{namespace}/decisions names the Play that holds them. | GET /{namespace}/decisions |
Plays that use it
| Play | What it does | Use it when |
|---|---|---|
| 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. | 20 or more nos a month recorded with a reason: lost Deals, cancels, downgrades and stalled trials |
Hub guides it deepens
| On startupbuilder.dev | Id |
|---|---|
| Customer research | process: |
| Signal interview | workflow: |
| Close the loop | workflow: |
Example
Since Jul 21 · 6 cancels · missing-feature 2, too-expensive 2, switched 1, no-longer-needed 1 · a 20-minute call with Aspen Row Oct 22 · owner statements shipped Oct 20 · close-the-loop note Oct 26 · sample Compiled, not written: edit its page, 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.