Switch kit
Wins your rival’s users with an importer tested on its exports and a dry run they see first, stating no rival fact without a source.
- Step
- Make the sale
- Channel
- Offers and Deals
- Summary
- startupbuilder.dev
Be our SDR: find companies that publicly run our rival, and write to them about moving with our importer, which shows a dry run first. State no fact about the rival I haven’t approved with its source. Over 200 units, the note comes from me. Hold 20 % back so we can tell if it works. 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":"switch-kit"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'switch-kit' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2
npx startup-builder do Campaign.create --json '{"play":"switch-kit"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'switch-kit' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2",
"mode": "try"
}
}application/json{
"type": "https: //startupbuilder.dev/ errors/ refused",
"title": "Refused",
"status": 422,
"detail": "switch-kit can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "52 companies a month publicly run the rival; the importer passes 41 of 43 fields",
"doc": "/plays/ switch-kit#check-use-when",
"fix": "Nothing to fix: one named rival, 40 or more a month.",
"then": "Metric.query"
},
…
Hand our latest rival import to the SDR agent and show me what it chose and why. Don’t send.
curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Monitor.checked%22%2C%20%22Post.published%22%2C%20%22File.uploaded%22%2C%20%22Collection.imported%22%2C%20%22Organization.announced%22%2C%20%22Review.published%22%2C%20%22Form.submitted%22%2C%20%22Visit.recorded%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_1302"}'
# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22lead_63%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%22lead_63%22" \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"import { $ } from 'startup-builder'
const { events: [last] } = await $.Event.list({ where: 'name in ("Monitor.checked", "Post.published", "File.uploaded", "Collection.imported", "Organization.announced", "Review.published", "Form.submitted", "Visit.recorded")', 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 (\"Monitor.checked\", \"Post.published\", \"File.uploaded\", \"Collection.imported\", \"Organization.announced\", \"Review.published\", \"Form.submitted\", \"Visit.recorded\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1302"}' --try # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"lead_63\""}' # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"lead_63\""}' # Not live yet (gaps #7){
"name": "do",
"arguments": {
"code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Monitor.checked\", \"Post.published\", \"File.uploaded\", \"Collection.imported\", \"Organization.announced\", \"Review.published\", \"Form.submitted\", \"Visit.recorded\")', 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_355",
"$type": "Decision",
"choice": "rescue-the-import",
"reason": "Brookline Rentals ran a dry run of 212 units from the rival’s export 26 hours ago and committed nothing; 3 lease dates did not map",
"stakes": "low",
"options": [
"rescue-the-import",
"switch-note",
"founder-note",
"import-with-them",
"not-a-fit",
"not-now"
…
sample · 4 Not live yetPreviewOn POST /{namespace}/processes/{process}/enableOff POST /{namespace}/processes/{process}/pauseTake this one POST /{namespace}/tasks/{task}/claim
action_2 is the rescue note with the dry-run report and the 3 unmapped dates, held 2 minutes; the last step waits 3 days and ends when they commit.
+4.3 per 100 · ■□□□ Too early to tell · n 118 · path → arr
| Try another: the company | Choice | Reason |
|---|---|---|
| Brookline Rentals: a dry run of 212 units, nothing committed in 26 hours | rescue-the-import | a dry run with nothing committed after 24 hours |
| Copperfield Homes, 60 units: a job post names the rival | switch-note | fits, under the 200-unit line |
| Westbridge Group, 480 units: a public review says they run the rival | founder-note | over the 200-unit line |
| Maple & Main: “The import missed our lease end dates.” | import-with-them | a failed import goes to a person |
| Riverbend Storage: runs the rival for storage units | not-a-fit | storage fits no Offer |
- Day 6: first import committed
- First signal
Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Switch Customers → New paying Customers → New Customers ↑ → ARR added each week → ARR
Worth’s formula: about +4.3 switch Customers per 100 companies reached × 70 companies a month × $497 first order ARR ≈ +$1,494. Under First signal: About 18 weeks to tell a +4 lift at 70 a month with 20 % held back (16 to read, 2 to mature); 90 days can tell +7 or more.
sample · api.sb/
Start it this week
npx startup-builder add play switch-kit # 1 · writes plays/switch-kit.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":"switch-kit"}'
# 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":"switch-kit"}'
# on
curl -X POST https://api.sb/leaseline.app/processes/outbound/enable \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
# off: no switch note sends; the importer and the versus page keep running
curl -X POST https://api.sb/leaseline.app/processes/outbound/pause \
-H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"| Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | Nothing to fix: one named rival, 40 or more a month. | POST /{namespace}/metrics/query |
domain-authenticated pass | Nothing to fix: the Domain is set up for email. | POST /{namespace}/domains/{domain}/authenticate |
mailbox-warm fail · Not live yet, gaps #1 | Wait for the warm-up to finish, 14 days from day 0; dry-run rescues keep running. | POST /{namespace}/mailboxes/{mailbox}/warm |
baseline warn · Not live yet, gaps #8 | n 1: your held-back 20 % becomes the comparison. | POST /{namespace}/metrics/query |
role-unstaffed warn · Not live yet, gaps #8 | Staff onboarding, or leave it: help with imports comes to you. | POST /{namespace}/roles/{role}/staff |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "switch-kit can’t start yet: 1 check fails and 2 warn.",
"checks": [
{
"check": "use-when",
"status": "pass",
"observed": "52 companies a month publicly run the rival; the importer passes 41 of 43 fields",
"doc": "/plays/switch-kit#check-use-when",
"fix": "Nothing to fix: one named rival, 40 or more a month.",
"then": "Metric.query"
},
{
"check": "domain-authenticated",
"status": "pass",
"observed": "leaseline.app, 3 records resolve",
"doc": "/plays/switch-kit#check-domain-authenticated",
"fix": "Nothing to fix: the Domain is set up for email.",
"then": "Domain.authenticate"
},
{
"check": "mailbox-warm",
"status": "fail",
"observed": "dana@leaseline.app, day 0 of 14",
"doc": "/plays/switch-kit#check-mailbox-warm",
"fix": "Wait for the warm-up to finish, 14 days from day 0; dry-run rescues keep running.",
"then": "Mailbox.warm"
},
{
"check": "baseline",
"status": "warn",
"observed": "1 switch Customer in 90 days, none asked",
"doc": "/plays/switch-kit#check-baseline",
"fix": "n 1: your held-back 20 % becomes the comparison.",
"then": "Metric.query"
},
{
"check": "role-unstaffed",
"status": "warn",
"roles": [
"onboarding-specialist",
"sales-lead"
],
"owned": 31,
"doc": "/plays/switch-kit#check-role-unstaffed",
"fix": "Staff onboarding, or leave it: help with imports comes to you.",
"then": "Role.staff"
}
]
}It says no, says why, and names the fix.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
use-when · domain-authenticated · pass: 52 a month run the rival, 41 of 43 fields import; the Domain resolves (gaps #8) | POST /{namespace}/metrics/query on switch-customers; POST /{namespace}/domains/{domain}/authenticate | 1 min | none |
mailbox-warm · fail: day 0 of 14 | POST /{namespace}/mailboxes/{mailbox}/warm; rescues of dry runs send meanwhile, because the Visitor started them | 0 min | 14 days |
baseline · warn: 1 switch in 90 days | POST /{namespace}/metrics/query; the held-back 20 % fills Before in Your number | 0 min | none |
role-unstaffed · warn: 31 of your open Leads have a person owner, you | POST /{namespace}/roles/{role}/staff, or leave it: import help comes to you, and the agent drafts into your queue for your 31 | 0 min | none |
Your number · Before 0 per 100 held back · Now 4.3 per 100 reached · Target 5 by Dec 31 · ■□□□ Too early to tell · n 118 matured
Each send costs $0.04 from your Domain (Worker cost); the importer’s nightly fixture run and the dry runs cost nothing. That comes to about $8 a month at 70 companies, 3 notes each over 3 months, and $5 at 40.
When it starts
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A company publishes that it runs the rival: a job post, a review or a case study | rival-user-announced | event on Post.published where mentions a rival on your roster | 30d; one company once | POST /{namespace}/monitors on the rival’s name, read each day |
| A signed-in Visitor’s dry run commits nothing for 24 hours | import-dry-run-abandoned | state on File.uploaded where dryRun.committedAt is null and dryRun.at < now - 24h | a state, read each hour; 72h | your importer calls POST /{namespace}/events/capture with File.uploaded |
| A new Customer’s first import, within 30 days, comes from the rival’s export | first-rival-import | event on Collection.imported where source = 'rival-importer' and customer.acceptedAt > now - 30d | 30d | your importer calls POST /{namespace}/events/capture |
+ 11 more for backlash, export changes, importer tests, named alternatives, rival pages and moments, asks, complaints, technology changes, disputes and pricing visits (?lanes=) |
{
"$id": "https://api.sb/leaseline.app/events/event_1302" ,
"$type": "Event",
"name": "File.uploaded",
"at": "2026-10-18T13:40:02Z" ,
"seq": 1302,
"data": {
"lead": "lead_63",
"kind": "dry-run-import",
"source": "rival-export",
"units": 212,
"mapped": 209,
"unmapped": [
"lease end date ×3"
],
"committed": false
}
}npx startup-builder trigger File.uploaded --like event_1302 # replays the dry run
npx startup-builder clock advance --by 24h # the dry run turns abandoned, and the rescue starts
npx startup-builder listen File.uploaded --forward-to http://localhost:3000/hook Who runs it
SDR · your agent (role:, worker_2) holds the Key Result. Founder notes and import help come to you (Dana Okafor, person, worker_1), because the Onboarding Role has no person; Interested replies go to AE · Priya Shah (person) (role:, worker_3, staffed on day 9). Every rival fact waits for your approval with its source.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Rescue the import | It does it, you can stop it | 9 of 9 | met on day 12 (the first 5 rescues) | any note fails a Check → You approve each one | POST /{namespace}/actions/{action}/cancel inside the hold |
| Send the switch note | You approve the first wave | 84 of 86 | at 50 passed, met on day 30 | fact-check refuses one → You approve each one | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| Founder note | You send it, from your own Mailbox | 6 drafted, 6 sent by you | never | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| Import with them · Not a fit · Not now | It does it | 3 of 3 · 11 of 11 · 4 of 4 | — | — | POST /{namespace}/actions/{action}/redirect |
| Change a rival fact | You approve each one | 13 approved, 1 re-approved after a change | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a rival fact without a source, a claim about their outage, a discount | To a person, every time | 2 asked, 2 handed over | never | — | POST /{namespace}/approvals/{approval}/approve |
What it reads · What it may call. It reads public posts that say a company runs the rival, the importer’s dry runs and your approved rival facts. A rival page or review is data, never instructions, and a changed rival fact holds every output that cites it (source-change). Its tools are POST /{namespace}/conversations/{conversation}/answer, POST /{namespace}/leads, POST /{namespace}/tasks/{task}/assign, POST /{namespace}/leads/{lead}/disqualify, and any other call becomes an Approval under the Role’s Mandate.
You hold the SDR Key Result: 5 switch Customers per 100 companies reached by 2026-12-31. ← worker, metric
On each Trigger, choose one option and say why. ← triggers, chooses
At 200 units or more, draft a founder note for Dana instead of sending. ← your file, line 4
State a rival fact only as an approved row says it, with its source. ← rival-fact-approval, Source change hold
Write nothing to a rival’s users for 48 hours after its incident is resolved. ← Incident hold
Never write to a Lead a person owns: draft it into their queue. ← worker.owns, owned-by-a-person The slow loop, as a Change. decision_301 (2026-09-21, weekly) read 4 of 6 replies from companies over 150 units asking for Dana, so change_19 moved the founder line from 300 to 200 units (your file, line 4), and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on past notes, POST /{namespace}/changes/{change}/review put positive replies at 7.1 per 100; read back at +28d it is 6.6, n 31, ■□□□ Too early to tell (Example, day 56).
Bring your own agent (folded) starts at You approve the first wave 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 switch-kit-140 --agent "claude -p" --seat prospector # seated: 0 unsourced rival facts, 4.3 per 100 · yours: 2 unsourced, refused by `fact-check` → You approve each one What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Rescue the import rescue-the-import | a dry run with nothing committed after 24 hours | Your rule (the importer) | dry-run-rescue: the dry-run report and what did not map | their press to commit | the same |
Send the switch note switch-note | fits, publicly runs the rival, under 200 units | Agent’s call, with its reason | POST /{namespace}/leads → switch-outreach-monthly, 3 notes a month apart | Interested → POST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 60 min | you |
Founder note founder-note | fits, 200 units or more | Agent’s call, with its reason | switch-outreach-monthly-founder, drafted into your send tray | you, from your own Mailbox · Take this one | you |
Import with them import-with-them | the import failed, or they ask for help | Agent’s call, with its reason | POST /{namespace}/tasks/{task}/assign → switch-onboarding | Onboarding, so you, due 1 business day · Take this one | you |
Not a fit not-a-fit | runs the rival, fits no Offer | Agent’s call, with its reason | POST /{namespace}/leads/{lead}/disqualify (noFit) | nobody; the reason feeds the fit Policy | the same |
Not now not-now | the rival’s incident is under 48 hours old, or another Play holds them | Your rule (Incident hold) | nothing; names the holder | nobody | the same |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | “Show me the import on our data.” | books the AE with the dry-run report in the Brief | AE · Priya Shah (person) | 60 min | POST /{namespace}/meetings/book |
| Send me pricing | “What would 120 units cost?” | answers with the Pricing Page’s list Price and one next step | SDR · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | “Our contract runs to March.” | the notes stop; it moves to Nurture until their renewal month | SDR · your agent | at once | POST /{namespace}/leads/{lead}/nurture |
| Wrong person | “Our ops lead, Sam, owns the software.” | asks them to bring Sam in; it never writes to an address from their text | SDR · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply until Oct 28 | the next note waits for the return date | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | “Can you move our maintenance history too?” | To a person: what the importer cannot map | Onboarding, so you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “Don’t email us again.” | nothing else sends, on any lane | nobody | at once | POST /{namespace}/contacts/{contact}/suppress |
When it hands to you
Founder notes and import help come to you; Interested replies go to AE · Priya Shah (person). One Brief per Role, as captured (brief and draft are Not live yet, gaps #7).
[
{
"$id": "https://api.sb/leaseline.app/tasks/task_536" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-15T17:00:00Z" ,
"on": "lead_59",
"brief": {
"who": "Westbridge Group, 480 units" ,
"said": "a public review: “we run the rival across 6 owners”" ,
"chose": "founder-note",
"why": "over the 200-unit line",
"draft": "in your send tray",
"never": [
"a rival fact without its source",
"their outage"
]
}
},
{
"$id": "https://api.sb/leaseline.app/tasks/task_519" ,
"$type": "Task",
"assignee": "worker_1",
"due": "2026-10-13T17:00:00Z" ,
"on": "lead_57",
"brief": {
"who": "Maple & Main, 96 units" ,
"said": "The import missed our lease end dates.",
"chose": "import-with-them",
"why": "a failed import goes to a person",
"unmapped": [
"lease end date ×14"
],
"next": "a 20-minute import call"
}
}
]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’s pending notes to that company cancel with claimedByPerson (Not live yet, gaps #7), and every reply goes to the claimer’s queue. A Task nobody claims by its due time goes up a Role through POST /{namespace}/tasks/{task}/escalate, and person-promise fires.
On time: you, 9 of 9 within their due time, median 3 h, n 9; AE · Priya Shah, 5 of 5 within 60 min, median 21 min, n 5, ■■□□ Early sign. Your queue: in week 1, 13 rival facts and 30 notes to approve; after the first wave, about 1 founder note and 1 import call a week.
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; Not live yet, gaps #8) | a send to a company whose Lead or 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 |
mailbox-warm (hub guard on POST /{namespace}/messages/{message}/schedule, gaps #1) | a first switch note from a Mailbox still warming | Wait for the warm-up to finish, 14 days from day 0; dry-run rescues keep running. | POST /{namespace}/mailboxes/{mailbox}/warm |
fact-check (hub guard on POST /{namespace}/content/{content}/publish) | a rival fact in a note or page that no approved, sourced row backs | Cut the claim, or name the record that backs it. | POST /{namespace}/content/draft |
source-change (the record’s Source change hold) | an output citing a rival value that changed at its source | Re-approve the changed row; outputs wait within 1 hour. | POST /{namespace}/changes/{change}/merge |
incident-hold (the record’s Incident hold) | a note to a rival’s user within 48 hours of its incident being resolved | It waits the 48 hours, then sends. | POST /{namespace}/messages/{message}/schedule |
| Stop (pauses the Play) | Pauses when | Then |
|---|---|---|
importer-test | a fixture run fails, or live errors pass 5 % of 20 imports in 24 h | the importer moves to assisted mode; its claims hold |
data-loss | a commit differs from its dry run by over 0.5 % | the importer pauses; the Customer hears within 1 h |
bounce | bounces pass 2 % of a day’s sends | POST /{namespace}/mailboxes/{mailbox}/rest |
complaint | complaints near 0.1 % | switch notes pause on every Mailbox |
reply-floor | positive replies under 1 % after 300 sends | notes stop; the importer and pages keep running |
hostile-reply | 3 hostile replies in 30 days | You approve each one |
credit-limit | switch credits reach the month’s Limit | grants pause; POST /{namespace}/approvals/{approval}/approve for you |
fit | switchers’ 90-day retention 15 points under other new Customers | the switch Offer pauses |
kill-switch | you press it | POST /{namespace}/processes/{process}/pause on outbound; the importer keeps running |
+ 3 more for the versus pages and rival-term ads (?lanes=) |
The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the rewrite that sent is the Copperfield 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 |
mailbox-warm pass · Not live yet, gaps #1 | — | POST /{namespace}/mailboxes/{mailbox}/warm |
JSON
{
"type": "https://startupbuilder.dev/errors/refused",
"title": "Refused",
"status": 422,
"detail": "The switch note to Copperfield Homes did not send: it says the rival raised its price 40 %, and no approved row says so.",
"checks": [
{
"check": "fact-check",
"status": "fail",
"doc": "/plays/switch-kit#check-fact-check",
"fix": "Cut the claim, or name the record that backs it.",
"then": "Content.draft"
},
{
"check": "mailbox-warm",
"status": "pass"
}
]
}It says no, says why, and names the fix.
[
{
"reason": {
"limit": "importer-test",
"observed": "2 of 43 fields failed after an export change",
"bound": "0 failed",
"window": "1 nightly run"
},
"doc": "/plays/switch-kit#stop-importer-test" ,
"task": "task_461"
}
]Did it work
Both halves come from one random assignment of the companies that publicly run the rival (rules m–o), and each figure’s call is POST /{namespace}/metrics/query by node and by arm; the parent field and every node above the leaf are Not live yet (gaps #4).
+4.3 per 100 · ■□□□ Too early to tell · n 118 · path → arr
- Chose
- rescue-the-import
- Why
- Brookline Rentals ran a dry run of 212 units from the rival’s export 26 hours ago and committed nothing; 3 lease dates did not map
- From
- rescue-the-import · switch-note · founder-note · import-with-them · not-a-fit · not-now
- Expects
- Collection.imported for lead_63 within 3d
- Reopens when
- no Collection.imported for lead_63 within 3d
- By
- worker_2
JSON
{
"$id": "decisions/decision_355",
"$type": "Decision",
"choice": "rescue-the-import",
"reason": "Brookline Rentals ran a dry run of 212 units from the rival’s export 26 hours ago and committed nothing; 3 lease dates did not map",
"stakes": "low",
"options": [
"rescue-the-import",
"switch-note",
"founder-note",
"import-with-them",
"not-a-fit",
"not-now"
],
"by": "worker_2",
"on": "lead_63",
"expected": "Collection.imported for lead_63 within 3d",
"reopensWhen": "no Collection.imported for lead_63 within 3d"
}| Node | Reached | Held back | Added | Confidence | Basis |
|---|---|---|---|---|---|
switch-customers | 4.3 per 100 (4 of 94) | 0 (0 of 24) | +4.3 [−1.6, 10.2] | ■□□□ Too early to tell | held back at random · n 118 · 22 still maturing · Other Startups’ results +3.9, ■■■□ Likely, n 2,310 |
new-paying-customers | 4.3 per 100 (4 of 94) | 0 (0 of 24) | +4.3 [−1.6, 10.2] | ■□□□ Too early to tell | the same assignment |
new-arr ↑ | $1,986, 4 Customers · Booked $3,278 with 2 rescued imports: credited, not all caused by this Play | $0 | +$1,986 [−$700, +$4,700] · Worth about +$1,500 a month | ■□□□ Too early to tell (4 vs 0 Customers) | the same · 4 trials still deciding |
net-new-arr · arr | +$1,986 | $0 | +$1,986, carried: it moves no other bridge term | ■□□□ Too early to tell | carried from new-arr |
The leaf counts new Customers whose first committed import came from the rival’s export within 30 days of accepting an Offer, per 100 companies assigned, matured 30 days after the first note. It runs to 400 matured in all; at 118 so far the Experiment runs on.
How it moves it: the company sees its own units and leases moved in a dry run (209 of 212 for Brookline) before it commits, and every rival fact it reads cites its source.
Guardrails: treated against held back: trials started per switch Customer, 2.0 against no trials, and opt-outs, 0.9 % of sends; switchers’ 30-day retention is 100 % (6 of 6) against 97 % for other new Customers.
{
"$id": "https://api.sb/leaseline.app/campaigns/switch-kit" ,
"$type": "Campaign",
"play": "play:switch-kit@1" ,
"path": [
"switch-customers",
"new-paying-customers",
"new-arr",
"net-new-arr",
"arr"
],
"experiment": {
"metric": "switch-customers",
"unit": "per-100",
"heldBack": 28,
"decision": null
},
"readout": {
"value": 4.3,
"n": 118,
"maturing": 22,
"interval": [
-1.6,
10.2
],
"readability": "C",
"basis": "held-back",
"pooled": false
},
"ledger": {
"spend": 7,
"workerCost": 5,
"hours": 3.1,
"pipelineArr": 1388,
"wonArr": 3278,
"projectedArr": 1494,
"paybackDays": 2
}
}Outside benchmark: 38 % of software buyers who switched tools name data migration as the hardest step (a buyer survey of 1,100 companies, 2026-06-30).
Before it · After it. Before it: editorial-seo and paid-search share the rival Triggers and send searchers to the versus page. After it: review-requests hears the switch Customers at day 30, and written-references adds them to the pool; usage-upsell hears them once they pace past Pro.
Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days). 140 companies that publicly run the rival were assigned at random, 112 reached and 28 held back; 118 have matured and 22 are still maturing. Separately, 9 dry-run imports from Visitors to the versus page were abandoned and rescued, outside the assignment. Read every captured thread at ?threads=; three follow the tables.
| What happened, by option | Decisions | Replies or imports | Won (credited, not all caused by this Play) |
|---|---|---|---|
| Send the switch note · Founder note | 86 · 6 | 6 positive replies · 3 replies to Dana | 3 on Pro, 1 on Team (+$1,986 ARR) |
| Rescue the import | 9 | 4 committed after the rescue | 1 on Pro, 1 on Team (+$1,292 ARR) |
| Import with them · Not a fit · Not now | 3 · 11 · 4 | 3 import calls · — · 4 sent after the 48 hours | 4 trials still deciding |
At 40 a month (holdout:) | about 75 in 56 days | held back at random; 90 days tells +9 or more | Other Startups’ results: +3.9 per 100, ■■■□ Likely, n 2,310 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_119 Campaign.created | the three steps; dry run fails mailbox-warm | 15 min |
| 0–2 | event_133 Change.merged | you approved 13 rival facts, each with its source | 25 min |
| 6 | event_214 Collection.imported | the first import committed after a rescue: Halston Flats, 88 units | — |
| 14 | event_402 Mailbox.warmed | the warm-up ends; the first wave of switch notes sends after your approval | 20 min |
| 28 · 56 | event_545 Change.merged · event_998 Readout.created | the 200-unit founder line: replayed at 7.1 positive per 100, read back at +28d at 6.6, n 31 | 5 min |
| 37 | event_640 Campaign.paused | importer-test: 2 of 43 fields failed after an export change; fixed and resumed in 5 h | 30 min |
| 1–56 | event_131 Approval.created, first of 50 | you approved the first wave; weekly reviews, from the POST /{namespace}/tasks/next log | 1.6 h · total 3.1 h measured, 3.2 h asked |
Leaseline → Brookline Rentals, Oct 19, 13:46 · Your dry run moved 209 of your 212 units, with every owner and lease.¹ Three lease end dates did not map, and the report shows which.² Fix them in the file and commit, or reply and Dana walks you through it.³ *Signed with the Role’s disclosure line.*⁴
Brookline, Oct 19, 15:10 · fixed the 3 dates and committed.
Collection.imported, 212 units; a Pro trial started.
- Their price went up 40 % last month, and most teams are leaving.
+ Leaseline imports your units, owners and leases from your current tool’s export, and you see the dry run before anything moves. Maple & Main, 2026-10-12 10:05 · “The import missed our lease end dates.” · Leaseline, 10:06 · *Thanks. Dana, our founder, handles imports and replies by 17:00 tomorrow.*⁵ The Brief went to Dana (
task_519); she held the import call at 14:00 the same day.
¹ Only counts from their own dry run. ² What did not map, from the dry-run report. ³ One next step. ⁴ The fixed signature line, compiled from the Role’s disclosure Setting. ⁵ The routed line, compiled from the Role’s Settings.
Your settings
---
holdout: 0.2 # 20 % of the companies that run the rival hear nothing, so we can tell
---
Write to companies that publicly run the rival, with the importer’s dry run. At 200 units or more, the note is Dana’s.
State a rival fact only as an approved row says it.$ npx startup-builder show plays/switch-kit
holdout 0.2 · founder line 200 units · rival facts approved your file plays/switch-kit.mdx:2,4,5 (Ask 2026-08-24; line 4 by change_19, 2026-09-21)
family demand · process outbound · worker role:prospector, owns never · chooses 6 (2 rule, 4 read) · sequences switch-outreach-monthly, switch-outreach-monthly-founder, switch-onboarding, dry-run-rescue · checks owned-by-a-person, mailbox-warm, fact-check, source-change, incident-hold · stops 12 (9 on outreach and the importer) · placements 16 catalog play:switch-kit@1 (Not live yet, gaps #5) Every key from the catalog
Compiled, not written: edit the Play record switch-kit 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.