Skip to content

Run it beside the old tool

Runs a new Customer’s work beside their old tool every day, fixes each difference you caused, and books the switch when the report reads zero.

Channel
Onboarding
Run oneMode try
You, to your agent

Hand our latest switching Customer’s run 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(%22Order.paid%22%2C%20%22Integration.connected%22%2C%20%22Integration.synced%22%2C%20%22Monitor.checked%22%2C%20%22Integration.revoked%22%2C%20%22Subscription.unsubscribed%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_9910"}'

# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22customer_71%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_71%22" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
import { $ } from 'startup-builder'

const { events: [last] } = await $.Event.list({ where: 'name in ("Order.paid", "Integration.connected", "Integration.synced", "Monitor.checked", "Integration.revoked", "Subscription.unsubscribed")', 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 (\"Order.paid\", \"Integration.connected\", \"Integration.synced\", \"Monitor.checked\", \"Integration.revoked\", \"Subscription.unsubscribed\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_9910"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"customer_71\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"customer_71\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Order.paid\", \"Integration.connected\", \"Integration.synced\", \"Monitor.checked\", \"Integration.revoked\", \"Subscription.unsubscribed\")', 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"
  }
}
Response200application/json
{
  "decision": {
    "$id": "https://api.sb/leaseline.app/decisions/decision_866",
    "$type": "Decision",
    "choice": "book-the-cutover",
    "reason": "Ashgrove Lettings’ run read 0 open differences for 5 run days over 170 compared notices; all 4 ours patterns are fixed",
    "stakes": "medium",
    "options": [
      "start-the-run",
      "standard-onboarding",
      "fix-ours",
      "ask-the-choice",
      "meet-on-a-stall",
      "book-the-cutover",
  …
sample · 4 Not live yetPreview

On PATCH /{namespace}/campaigns/{id}Off PATCH /{namespace}/campaigns/{id}Take this one POST /{namespace}/tasks/{task}/claim

action_2 is the cutover ask to Ashgrove’s admin, waiting for your approval because the stakes are medium. It asks them for a date; nothing Leaseline makes reaches their live records before it.

77.8 cutovers per 100 switchers against 37.5 before · before-after, not proof · n 9 · path → arr

Try another: the CustomerChoiceReason
Ashgrove Lettings: 0 open differences for 5 run days, 170 noticesbook-the-cutovera zero streak over 100 items
Linden Court: paid Pro; the old tool sends 34 notices a daystart-the-runa switcher with read-only access
Ashgrove, day 4: 9 notices dated a day late on our sidefix-oursa difference you caused, listed first
Rowan Place: open differences flat at 7 for 5 run daysmeet-on-a-stalla stall gets one Meeting
Quarry Homes: paid Pro, keeps everything in paper filesstandard-onboardingnothing to compare against

Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Cutover rate → Month-3 retention → Lost Customers ↓ → ARR added each week → ARR

Worth’s formula: about +22 cutovers per 100 switchers (Other Startups’ results; no lift of your own yet) × 6 switchers a month × 0.5 kept to month 3 that would have left × $347 Pro ARR ≈ +$229 of churn avoided. Under First signal: At 6 switchers a month, 90 days can tell only +40 per 100 or more, so your file starts at holdout: 0 and reads before-after.

sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Monitor.checked · day-0 difference report 3 h after connect · 8 keys Not live yet

Start it this week

Terminal
npx startup-builder add play run-beside   # 1 · writes plays/run-beside.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":"run-beside"}'

# 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":"run-beside"}'

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

# off: open runs end with their report; switchers get standard onboarding
curl -X PATCH https://api.sb/leaseline.app/campaigns/run-beside \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":"now"}'
422Refusedrun-beside can’t start yet: 1 check fails and 2 warn.
CheckFixThen
use-when pass · Not live yet, gaps #8Keep 4 or more switchers a month, or use standard onboarding.POST /{namespace}/metrics/query
read-only failConnect the old tool with its read-only export scope instead.POST /{namespace}/integrations/connect
baseline warn · Not live yet, gaps #8n 8: it reads before-after, not proof, until volume allows a held-back half.POST /{namespace}/metrics/query
role-unstaffed warn · Not live yet, gaps #8Staff the CSM, or leave it: stalls come to you, and Priya’s 4 get drafts.POST /{namespace}/roles/{role}/staff
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "run-beside can’t start yet: 1 check fails and 2 warn.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "41 Customers; 6 switchers a month name an old tool at intake",
      "doc": "/plays/run-beside#check-use-when",
      "fix": "Keep 4 or more switchers a month, or use standard onboarding.",
      "then": "Metric.query"
    },
    {
      "check": "read-only",
      "status": "fail",
      "observed": "the old-tool connection asks for write access",
      "doc": "/plays/run-beside#check-read-only",
      "fix": "Connect the old tool with its read-only export scope instead.",
      "then": "Integration.connect"
    },
    {
      "check": "baseline",
      "status": "warn",
      "observed": "3 of 8 switchers retired the old tool within 45 days, in 90 days",
      "doc": "/plays/run-beside#check-baseline",
      "fix": "n 8: it reads before-after, not proof, until volume allows a held-back half.",
      "then": "Metric.query"
    },
    {
      "check": "role-unstaffed",
      "status": "warn",
      "roles": [
        "success-manager",
        "customer-lead"
      ],
      "owned": 4,
      "doc": "/plays/run-beside#check-role-unstaffed",
      "fix": "Staff the CSM, or leave it: stalls come to you, and Priya’s 4 get drafts.",
      "then": "Role.staff"
    }
  ]
}

It says no, says why, and names the fix.

CheckFix (one call)Your timeWait
use-when · baseline · pass: 6 switchers a month; warn: 3 of 8 in 90 days (gaps #8)POST /{namespace}/metrics/query on cutover-rate, which fills Before in Your number1 minnone
read-only · fail: the connection asks for writePOST /{namespace}/integrations/connect with the read-only export scope10 minthe first backfill, about 3 h
role-unstaffed · warn (gaps #8): 4 of your Customers have a person owner, PriyaPOST /{namespace}/roles/{role}/staff, or leave it: stalls come to you, and Priya’s 4 get drafts in her queue0 minnone

Your number · Before 37.5 · Now 77.8 cutovers per 100 switchers · Target 80 by Dec 31 · before-after, not proof · n 9 matured

Each run costs about $1.10 in shadow compute and model calls on differences, and each update $0.04 (Worker cost). The miss rule refunds the first month: 1 so far, $29. That comes to about $7 a month at 6 switchers, plus refunds.

When it starts

WhenTrigger idStarts onWindowHow it gets in
A Customer pays while their old tool still does the workswitcher-first-orderevent on Order.paid where the first paid Order, while the intake or the Deal names an old tool that still does the workset up within 7dPOST /{namespace}/integrations/connect (Stripe) for the payment; the intake names the old tool
Both tools hold the same run day’s noticesrun-day-syncedstate on Integration.synced where both sides of an open run hold the same run day's itemscompared within 1dthe old tool’s read-only export, through POST /{namespace}/integrations/{integration}/sync
0 open differences for 5 run days, over 100 itemszero-streakthreshold on Monitor.checked where 0 open differences for 5 run days in a row, over 100 or more items, every ours pattern fixedthe cutover ask within 2dthe daily difference report
+ 5 more for connects, stalls, day 45, the old tool going off and cancels (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_9910",
  "$type": "Event",
  "name": "Monitor.checked",
  "at": "2026-10-18T07:00:00Z",
  "seq": 9910,
  "data": {
    "customer": "customer_71",
    "run": "run_9",
    "open": 0,
    "streak": 5,
    "compared": 170,
    "oursFixed": 4,
    "day": 23
  }
}
Reproduce it in test
npx startup-builder trigger Monitor.checked --like event_9910   # replays the fifth zero day
npx startup-builder clock advance --by 2d   # runs the cutover ask’s day-2 step
npx startup-builder listen Monitor.checked --forward-to http://localhost:3000/hook

Who runs it

CSM · your agent (role:success-manager, worker_5) holds the Key Result. Stalls go to the Head of customers Role, which has no person, so you (Dana Okafor, person, worker_1), and so do day-45 refunds and every cutover ask, which you approve. The 4 Customers AE · Priya Shah (person) owns get drafts in her queue. Your number links to Did it work.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Start the run · Standard onboardingIt does it11 of 11 · 3 of 3——POST /{namespace}/actions/{action}/redirect
Fix what you caused · Ask the choice onceIt does it, you can stop it13 of 13 · 21 of 22 (1 redirected)met on day 16 (the first wave)a fix fails the eval set → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Meet on a stall · Book the cutover · End the run and refundYou approve each one2 of 2 · 7 of 7 · 1 of 1never: the switch is the Customer’s date—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Change the run sheet’s fieldsThe Customer’s written yes1 addednever—POST /{namespace}/tasks/{task}/claim
Never: write to the old tool, zero errors claimed, a cancel that waitsRefused, every time1 draft refusednever—nothing; the Checks hold it

What it reads · What it may call. It reads the old tool through its read-only export and only the fields the run sheet lists; a report view or a login never appears in a Message. Nothing it makes reaches live records before the cutover date the Customer confirms (shadow-hold). Its tools are POST /{namespace}/integrations/connect, POST /{namespace}/issues/{issue}/triage, POST /{namespace}/changes/{change}/review, POST /{namespace}/conversations/{conversation}/answer, POST /{namespace}/meetings/book, POST /{namespace}/tasks/{task}/assign, POST /{namespace}/integrations/{integration}/sync, POST /{namespace}/invoices/{invoice}/refund, and any other call becomes an Approval under the Role’s Mandate.

The brief (folded): compiled Agent-register Markdown · play:run-beside@1 · each line names its record key
You hold the CSM Key Result: 80 cutovers per 100 switchers by 2026-12-31.        ← worker, metric
On each Trigger, choose one option and say why; a held Customer gets Not now.    ← triggers, chooses, keep-first
Compare due date, amount and recipient on every run day’s notices.               ← your file, line 4
List the differences you caused first, each with its evidence.                   ← copyRules 2, 3
Say zero open differences over a count of items and days, never zero errors.     ← copyRules 4
A stall gets one Meeting with Dana; day 45 with no streak refunds the month.     ← your file, line 5

The slow loop. The weekly Decisions kept the run sheet’s fields and the 5-day streak, and no Change was merged in the window, so there is no replay or read-back to print. Each ours fix shipped through POST /{namespace}/changes/{change}/review against the Product’s eval set, never as a change to this Play.

Bring your own agent (folded) starts at It does it, you can stop it for difference classing when it matches the seated agent on the eval suite; cutover asks stay at You approve each one. POST /{namespace}/roles/{role}/staff seats it.

Terminal
npx startup-builder eval --scenario run-beside-11 --agent "claude -p" --seat success-manager   # seated: 7 of 9 cut over, every ours fix eval-gated · yours: 6 of 9, 1 difference classed theirs that was ours

What it can do next

OptionPicked whenDecided byRunsHands toIf it’s just you
Start the run start-the-runa switcher’s first paid Order, read-only access, 20+ items a dayYour rule (line 3)POST /{namespace}/integrations/connect read-only → the 14-day backfill → parallel-run-updatesnobodythe same
Standard onboarding standard-onboardingnothing checkable, under 20 items a day, or no read-only accessYour rule (line 3)onboard-customernobodythe same
Fix what you caused fix-oursa difference classed oursAgent’s call, with its reasonPOST /{namespace}/issues/{issue}/triage → POST /{namespace}/changes/{change}/review against the eval setnobody; the fix ships when every case passesthe same
Ask the choice once ask-the-choicea difference classed choice, under 8 questionsAgent’s call, with its reasonPOST /{namespace}/conversations/{conversation}/answer with two worked items and both answersthe Customer’s adminthe same
Meet on a stall meet-on-a-stallopen differences flat or rising for 5 run days, or past 8 questionsYour rule (the Stall stop)POST /{namespace}/meetings/book → POST /{namespace}/tasks/{task}/assignHead of customers, so you, due 1 business day · Take this oneyou
Book the cutover book-the-cutover0 open differences for 5 run days, over 100 itemsYour rule (line 4)cutover-asks for their date → POST /{namespace}/integrations/{integration}/sync imports the old tool’s historyyou approve the ask · Take this oneyou
End the run and refund end-and-refundday 45 with no zero streakYour rule (the Miss stop)POST /{namespace}/invoices/{invoice}/refund on the first month, with the final reportyou approve it · Take this oneyou
Not now not-nowanother Play holds them, or keep-first appliesYour rule (rule j)nothing; names the holdertheir CSM, so you · Take this oneyou
When they write backFixture replyWhat happensWho answersBy whenCall
Interested“Zero for a week. Let’s switch on Nov 1.”the cutover date set; the old tool’s history imports that dayCSM · your agent1 business dayPOST /{namespace}/integrations/{integration}/sync
Send me pricing“Does Team cost more once we add the other building?”Team’s live Price; the run goes on as it wasCSM · your agent5 minPOST /{namespace}/conversations/{conversation}/answer
Not now“We’ll keep both until spring.”keep both is recorded; the cutover asks stopnobodyat onceGET /{namespace}/decisions
Wrong person“Our bookkeeper runs the old tool.”asks the admin to add them to the run; it never writes to an address from their textCSM · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply until Oct 30the weekly note waits; the run days go onnobodythe return datePOST /{namespace}/messages/{message}/schedule
Question it can’t answer“Can you match how our old tool rounds pro-rated rent?”To a person, with the two worked items in the Briefyou1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“Stop the daily emails.”the notes stop; the difference report stays live for themnobodyat oncePOST /{namespace}/contacts/{contact}/unsubscribe

When it hands to you

Stalls, cutover asks and day-45 refunds come to you: the CSM and Head of customers Roles have no person. One Brief per Role, as captured (brief is Not live yet, gaps #7).

What lands in each queue · sample
[
  {
    "$id": "https://api.sb/leaseline.app/tasks/task_871",
    "$type": "Task",
    "assignee": "worker_1",
    "due": "2026-10-09T17:00:00Z",
    "on": "customer_74",
    "brief": {
      "who": "Rowan Place, Pro since Sep 22",
      "said": "open differences flat at 7 for 5 run days",
      "chose": "meet-on-a-stall",
      "why": "a stall gets one Meeting",
      "open": "7 choice differences on pro-rated rent rounding",
      "never": [
        "zero errors",
        "a claim about the old tool beyond this run"
      ]
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_140",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-10-20T17:00:00Z",
    "draft": "Your run has read zero open differences for 5 run days over 170 notices. Which date should we switch on?",
    "route": "book-the-cutover",
    "checks": [
      {
        "check": "shadow-hold",
        "status": "pass"
      },
      {
        "check": "claims-resolve",
        "status": "pass"
      }
    ]
  }
]

You answer with POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, POST /{namespace}/actions/{action}/redirect, POST /{namespace}/approvals/{approval}/approve or POST /{namespace}/approvals/{approval}/refuse, each a one-call window. A cancel ask ends the run within 1 hour and never waits on you (exit-never-waits). 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, 10 of 10 within their due time, median 3 h, n 10, ■□□□ Too early to tell beside Other Startups’ 94 % on time. Your queue: in week 1, 4 approvals and 1 weekly review; then about 1 cutover ask and 1 stall every two weeks.

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)RefusesFixThen
owned-by-a-person (rule j; gaps #8)an update to a Customer whose open Deal a person owns, or whose Conversation a person claimedLeave 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 cutover ask to a Customer who is at risk, past due or has an unsubscribe pendingLet 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 Playreview-requests holds them until Nov 4.GET /{namespace}/decisions
exit-never-waits (the record’s Cancel pass-through; gaps #3)any step in front of a cancel while a run is openRemove the step: the run ends within 1 h and the cancel completes.POST /{namespace}/subscriptions/{subscription}/unsubscribe
read-only (the record’s Read-only scope)a credential for the old tool that grants writeConnect the old tool with its read-only export scope instead.POST /{namespace}/integrations/connect
shadow-hold (the record’s Shadow hold)any output reaching live records before the cutover date the Customer confirmedKeep it in the shadow run until their date.GET /{namespace}/decisions
field-scope (the record’s Field scope)a read of a field the run sheet does not listAsk the admin once in writing; the field joins on their yes.POST /{namespace}/conversations/{conversation}/answer
regression (the record’s Regression stop)a fix that fails any case in the Product’s eval setFix it until every eval case passes; it ships to no account until then.POST /{namespace}/changes/{change}/review
copy-deletion (the record’s Copy deletion)a read after the old tool is disconnected, or copied outputs kept off the Customer’s accountNothing to do: reads stop that hour; one action removes the copies.POST /{namespace}/integrations/{integration}/revoke
claims-resolve (hub)a sentence about the old tool beyond this run’s recorded differences, or zero errors claimedSay zero open differences over the items and days counted.POST /{namespace}/content/draft
Stop (pauses the Play)Pauses whenThen
stallopen differences flat or rising for 5 run daysthe weekly note pauses; a Task to you and a Meeting proposed
missno zero streak by day 45the run ends with its final report; the first month is refunded
kill-switchyou press itevery Tactic pauses; open runs keep their report
+ 1 more for the email lane: the question cap (?lanes=all)

The captured refusal, then the Stop that fired as a Campaign.paused reason (Not live yet, gaps #2); the update that sent is the Linden Court thread in Example.

422RefusedThe update to Linden Court did not send: it says their old tool makes errors.
CheckFixThen
claims-resolve failSay zero open differences over the items and days counted.POST /{namespace}/content/draft
keep-first pass · Not live yet, gaps #8—POST /{namespace}/tasks/next
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The update to Linden Court did not send: it says their old tool makes errors.",
  "checks": [
    {
      "check": "claims-resolve",
      "status": "fail",
      "doc": "/plays/run-beside#check-claims-resolve",
      "fix": "Say zero open differences over the items and days counted.",
      "then": "Content.draft"
    },
    {
      "check": "keep-first",
      "status": "pass"
    }
  ]
}

It says no, says why, and names the fix.

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "reason": {
      "limit": "stall",
      "observed": "7 open, flat",
      "bound": "5 run days",
      "window": "run_11, Rowan Place"
    },
    "doc": "/plays/run-beside#stop-stall",
    "task": "task_871"
  }
]

Did it work

At 6 switchers 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. Each figure’s call is POST /{namespace}/metrics/query by node; the leaf and every node above it are Not live yet (gaps #4).

77.8 cutovers per 100 switchers against 37.5 before · before-after, not proof · n 9 · path → arr

Chose
book-the-cutover
Why
Ashgrove Lettings’ run read 0 open differences for 5 run days over 170 compared notices; all 4 ours patterns are fixed
From
start-the-run · standard-onboarding · fix-ours · ask-the-choice · meet-on-a-stall · book-the-cutover · end-and-refund · not-now
Expects
the old tool off by the cutover date Ashgrove gives, and still paying in month 3
Reopens when
no Integration.revoked for customer_71 within 14d of the cutover date
By
worker_5
JSON
{
  "$id": "decisions/decision_866",
  "$type": "Decision",
  "choice": "book-the-cutover",
  "reason": "Ashgrove Lettings’ run read 0 open differences for 5 run days over 170 compared notices; all 4 ours patterns are fixed",
  "stakes": "medium",
  "options": [
    "start-the-run",
    "standard-onboarding",
    "fix-ours",
    "ask-the-choice",
    "meet-on-a-stall",
    "book-the-cutover",
    "end-and-refund",
    "not-now"
  ],
  "by": "worker_5",
  "on": "customer_71",
  "expected": "the old tool off by the cutover date Ashgrove gives, and still paying in month 3",
  "reopensWhen": "no Integration.revoked for customer_71 within 14d of the cutover date"
}
NodeNow (56 days)Before (the 90 days, per 56)AddedConfidenceBasis
cutover-rate77.8 per 100 (7 of 9 matured switchers) · 2 still inside 45 days37.5 (3 of 8)— · Worth about +$230 a month, kept—before-after, not proof · Other Startups’ results +22 per 100, ■■■□ Likely, n 1,140
month-3-retention8 of 9 still paying in month 35 of 8——before-after; 2 still maturing
churned-arr ↓$347 lost, 1 Customer · Booked: none; this node counts what left$1,041, 3 Customers——before-after; lower is better
net-new-arr · arrcarried from churned-arrcarried——carried

Before against after reads one window against the 90 days before it, scaled to 56 days; it is not proof, and Other Startups’ results print beside it. The leaf counts switching Customers who start a run and retire the old tool within 45 days of their first paid Order.

How it moves it: each switcher saw every difference between the two tools listed and fixed, ours first, and reached a zero streak in a median of 21 days.

Guardrails: before against after, the family pair under churned-arr: contraction-arr, $0 from switches down against $347 before, and contribution-margin, run cost $1.10 a Customer against $29 a month.

The Campaign card · sample
{
  "$id": "https://api.sb/leaseline.app/campaigns/run-beside",
  "$type": "Campaign",
  "play": "play:run-beside@1",
  "path": [
    "cutover-rate",
    "month-3-retention",
    "churned-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "cutover-rate",
    "unit": "per-100",
    "heldBack": 0,
    "decision": null
  },
  "readout": {
    "value": null,
    "n": 9,
    "maturing": 2,
    "interval": null,
    "readability": "inadmissible",
    "basis": "before-after",
    "pooled": true
  },
  "ledger": {
    "spend": 29,
    "workerCost": 13,
    "hours": 1.6,
    "pipelineArr": 0,
    "wonArr": 0,
    "projectedArr": 229,
    "paybackDays": null
  }
}

Outside benchmark: this Play’s catalog record carries no sourced range yet, so Other Startups’ results stand in: +22 cutovers per 100 switchers, ■■■□ Likely, n 1,140.

Before it · After it. Before it: switch-kit and paid-pilot bring the switchers. After it: review-requests asks at the cutover, customer-referrals hears the Customers who switched, and usage-upsell takes them past their Limit; keep-first holds a Customer at risk.

Example

Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days): 14 switchers paid, 3 went to standard onboarding, and 11 started a run; 9 have passed day 45. Read every captured thread at ?threads=all; three follow the tables.

What happened, by optionDecisionsResult (counts)Kept (credited, not all caused by this Play)
Start the run · Standard onboarding11 · 311 runs · 3 onboarded—
Fix what you caused · Ask the choice once13 · 2213 ours patterns fixed · 21 answered—
Meet on a stall · Book the cutover · End the run and refund2 · 7 · 12 Meetings · 7 cut over · 1 refunded ($29)7 of 7 still paying
All switchers · before9 matured · 87 cut over by day 45 · 38 paying in month 3 · 5
At 4 switchers a month (the Use it when volume)about 7 in 56 daysbefore-after, not proofOther Startups’ results: +22 per 100, ■■■□ Likely, n 1,140
DayEventWhat happenedYour time
0event_9802 Campaign.createdthe three steps; the dry run fails read-only15 min
0event_9805 Integration.connectedthe old tool’s read-only export; the first backfill took 3 h10 min
16event_9840 Role.updatedpromoted: fixes and choice questions go out after a 2-minute hold—
19event_9852 Monitor.checkedthe first zero streak: Linden Court, 5 days over 168 notices—
45event_9888 Campaign.pausedstall on Rowan Place: 7 open for 5 run days; you met them on Oct 930 min
55event_9910 Monitor.checkedAshgrove’s zero streak, 170 notices; the cutover ask waits for your approval—
1–56event_9812 Approval.created, first of 11you approved 7 cutover asks, 1 refund and the first wave; claims-resolve refused 1 update; weekly reviews55 min · total 1.6 h measured, 1.5 h asked

Leaseline → Linden Court’s admin, Sep 13 08:00 · Day 19 of your run: 0 open differences for 5 run days, over 168 renewal notices compared on due date, amount and recipient.¹ The 4 we caused are fixed and listed in your report.² Which date should we switch on? *Signed with the Role’s disclosure line.*³

Linden Court, Sep 13 10:20 · “Let’s switch on Oct 1.” The old tool went off on Oct 1; Linden Court is still paying.

Linden Court, 2026-09-04 · the update claims-resolve refused, then the update that sent
- Good news: we found 6 errors in your old tool that Leaseline gets right.
+ Day 10: 6 open differences, all on the old tool’s side, each listed with the notice it came from.

Rowan Place, 2026-10-08 07:00 · 7 open differences flat for 5 run days, all on pro-rated rent rounding · The agent, 07:01 · paused the weekly note and assigned the stall to you (task_871), due 17:00 · Dana, 09:30 · claimed it and booked a 20-minute call for Oct 9; the rounding became a Setting.

¹ Zero open differences over a stated count of items and days, never zero errors. ² The differences you caused, listed first, each with its evidence. ³ The fixed signature line, compiled from the Role’s disclosure Setting.

Your settings

plays/run-beside.mdx
---
holdout: 0        # 6 switchers a month is too few to hold half back; it reads before-after
---
Run beside any switcher with read-only access and 20 or more notices a day.
Compare due date, amount and recipient; book the switch after 5 zero days over 100 notices.
A stall gets one call with Dana; day 45 with no streak refunds the first month.
Text
$ npx startup-builder show plays/run-beside
holdout 0 · 20 notices a day · 3 fields · 5 zero days over 100 · stall call · day-45 refund     your file  plays/run-beside.mdx:2,4,5,6 (Ask 2026-08-24)
worker role:success-manager, owns never · chooses 8 (6 rule, 2 read) · sequences parallel-run-updates, cutover-asks · approval first-send, every-send · checks owned-by-a-person, keep-first, customer-ask-cap, exit-never-waits, read-only, shadow-hold, field-scope, regression, copy-deletion, claims-resolve · stops 3 · placements gmail, outlook + the difference report    catalog  play:run-beside@1 (Not live yet, gaps #5)

Every key from the catalog

KeyIdWhat it is
Tacticparallel-run-setupParallel run setup: Scopes one parallel run on a signed run sheet, connects the old tool read-only, and backfills 14 days into a day-0 difference report. Starts at You approve the first wave.
Tacticdaily-differenceDaily difference: Compares each run day’s items from both tools field by field, classes every difference as ours, theirs, choice or mapping, and refreshes the difference report. Starts at You approve the first wave.
Tacticdifference-burn-downDifference burn-down: Closes open differences: an eval-gated fix per ours pattern, a Setting per choice answer, a Mapping fix per gap, and one Meeting for a stall. Starts at You approve each one.
Tacticcutover-handoffCutover handoff: Turns a zero streak into a dated cutover with the old tool’s history imported, or ends a missed run on day 45 with a refund. Starts at You approve each one.
Tacticclaims-checkClaims check maps every draft sentence to an approved source or the Person’s own words, and holds anything else before it reaches anyone. Starts at Sends nothing.
Triggerswitcher-first-orderSwitcher first Order: event on Order.paid, window 7d.
Triggerold-tool-connectedOld tool connected: event on Integration.connected, window 14d.
Triggerrun-day-syncedRun day synced: state on Integration.synced, window 1d.
Triggerdifference-stalledDifference stalled: threshold on Monitor.checked, window 3d.
Triggerzero-streakZero streak: threshold on Monitor.checked, window 2d.
Triggerrun-deadlineRun deadline: state on Monitor.checked, window 1d.
Triggerold-tool-retiredOld tool retired: event on Integration.revoked, window 14d.
Triggerrun-cancel-askRun cancel ask: event on Subscription.unsubscribed, window 1d.
Sequenceparallel-run-updatesParallel run updates: 8 steps; it ends on The zero streak holds, and the cutover sequence starts.; The Customer cancels.; The Customer asks to stop the notes; the report stays live..
Sequencecutover-asksCutover asks: 5 steps; it ends on The old tool is disconnected or confirmed off.; The Customer answers keep both, and the answer is recorded.; The Customer cancels..

Compiled, not written: edit the Play record run-beside 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.