Skip to content

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.

Reasons, codedMode try
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"
  }
}
Response200application/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"
  }
}
samplePreview

What it does

What it doesPrice per unitMovesWho does itToday
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 churnHead of customersevery call live

Set it up

Check (dry run on a 0-Customer Startup, gaps #8)FixThenYour timeWait
use-when (Not live yet, gaps #8) · pass: 6 cancels with a reason in 90 daysRun the Play Not yet when names, or wait until the volume is there.POST /{namespace}/metrics/query1 minnone
role-unstaffed (Not live yet, gaps #8) · warn: Head of customers has no person: interviews come to youStaff Head of customers, or leave it: interviews come to you.POST /{namespace}/roles/{role}/staff0 minnone

The records

ReasonCustomersWhat answers it
missing-feature: owner statements2the Change merged Oct 20; a note to each (Win-back)
too-expensive2the $19 Starter plan, offered once (When someone cancels)
switched1objection-watch holds them until the reason stops being true
no-longer-needed1nothing sends
The reasons, coded on Monday
{
  "$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

CallHereEmitsStatus
GET /{namespace}/events6 cancels since Jul 21, each with its reason code—live
POST /{namespace}/collections/queryCollection.queriedlive
POST /{namespace}/meetings/booksignal-interview: 20 minutes with Aspen Row’s office managerMeeting.bookedlive
POST /{namespace}/messages/{message}/scheduleclose-the-loop: owner statements shipped, to the 2 who askedMessage.scheduledlive

Events

EventWhen it firesWhat hears it
Subscription.unsubscribeda cancel, with its reason codecoded every Monday by customer-research
Meeting.bookeda former Customer says yes to 20 minutesthe interview notes, on the reason

Refuses when

CheckRefusesFixThen
customer-ask-cap (Not live yet, gaps #3)a second open ask to one Customer in a window, from any PlayWait for the open ask to close; GET /{namespace}/decisions names the Play that holds them.GET /{namespace}/decisions

Plays that use it

PlayWhat it doesUse it when
Objection watchRecords 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.devId
Customer researchprocess:customer-research
Signal interviewworkflow:signal-interview
Close the loopworkflow:close-the-loop

Example

Leaseline, as the fixture ran it
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.

Contents