URL: /plays/switch-kit · Id: `play:switch-kit`

# 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.

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.

```text title="Set it up · Ask"
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.
```

```text title="Run one · Ask"
Hand our latest rival import to the SDR agent and show me what it chose and why. Don’t send.
```

```ts title="Run one · SDK"
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 }
```

```json title="Run one · Response · sample"
{
  "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"
    ],
    "by": "worker_2",
    "on": "lead_63",
    "expected": "Collection.imported for lead_63 within 3d",
    "reopensWhen": "no Collection.imported for lead_63 within 3d"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_2",
      "verb": "Lead.qualify",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingTime",
      "mode": "try",
      "by": "worker_2",
      "verb": "Message.schedule",
      "checks": [
        {
          "check": "owned-by-a-person",
          "status": "pass"
        },
        {
          "check": "mailbox-warm",
          "status": "pass"
        },
        {
          "check": "fact-check",
          "status": "pass"
        },
        {
          "check": "incident-hold",
          "status": "pass"
        }
      ]
    }
  ]
}
```

`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 |

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$1,500 ARR a month at 70 | 40 min week 1 (approving rival facts), then 15 a week | $0.04 per send, ~$8 a month at 70 companies | Day 6: first import committed | 1 named rival, 40+ companies a month publicly running it | No importer passes its fixtures: build it (<Op verb="Change.merge" />) · under 40 a month: `editorial-seo` |

**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/leaseline.app · 41 Customers at 2026-10-19 · on File.uploaded · first rescue note 24h 06m after the dry run · 8 keys Not live yet`

## Start it this week
```bash
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"
```


```json title="The dry run’s 422 · 2026-08-24 · sample"
{
  "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"
    }
  ]
}
```

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-use-when"></a>`use-when` · <a id="check-domain-authenticated"></a>`domain-authenticated` · pass: 52 a month run the rival, 41 of 43 fields import; the Domain resolves (gaps #8) | <Op verb="Metric.query" /> on `switch-customers`; <Op verb="Domain.authenticate" /> | 1 min | none |
| <a id="check-mailbox-warm"></a>`mailbox-warm` · fail: day 0 of 14 | <Op verb="Mailbox.warm" />; rescues of dry runs send meanwhile, because the Visitor started them | 0 min | 14 days |
| <a id="check-baseline"></a>`baseline` · warn: 1 switch in 90 days | <Op verb="Metric.query" />; the held-back 20 % fills Before in Your number | 0 min | none |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn: 31 of your open Leads have a person owner, you | <Op verb="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 | <Op verb="Monitor.create" /> 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 <Op verb="Event.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 <Op verb="Event.capture" /> |
| + 11 more for backlash, export changes, importer tests, named alternatives, rival pages and moments, asks, complaints, technology changes, disputes and pricing visits (`?lanes=all`) | | | | |

```json title="One signed delivery · StartupBuilder-Signature"
{
  "$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
  }
}
```

```bash title="Reproduce it in test"
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:prospector`, `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:sales-lead`, `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 | <Op verb="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 | <Op verb="Approval.approve" /> · <Op verb="Approval.refuse" /> |
| Founder note | You send it, from your own Mailbox | 6 drafted, 6 sent by you | never | — | <Op verb="Approval.approve" /> · <Op verb="Approval.refuse" /> |
| Import with them · Not a fit · Not now | It does it | 3 of 3 · 11 of 11 · 4 of 4 | — | — | <Op verb="Action.redirect" /> |
| Change a rival fact | You approve each one | 13 approved, 1 re-approved after a change | never | — | <Op verb="Change.merge" /> · <Op verb="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 | — | <Op verb="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 <Op verb="Conversation.answer" />, <Op verb="Lead.create" />, <Op verb="Task.assign" />, <Op verb="Lead.disqualify" />, and any other call becomes an Approval under the Role’s Mandate.

```md title="The brief (folded): compiled Agent-register Markdown · play:switch-kit@1 · each line names its record key"
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 <Op verb="Change.merge" />. **Replayed on past notes**, <Op verb="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; <Op verb="Role.staff" /> seats it.

```bash
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 | <Op verb="Lead.create" /> → `switch-outreach-monthly`, 3 notes a month apart | Interested → <Op verb="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 | <Op verb="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 | <Op verb="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 | <Op verb="Meeting.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 | <Op verb="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 | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply until Oct 28 | the next note waits for the return date | nobody | the return date | <Op verb="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 | <Op verb="Conversation.escalate" /> |
| Stop | “Don’t email us again.” | nothing else sends, on any lane | nobody | at once | <Op verb="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).

```json title="What lands in each queue · sample"
[
  {
    "$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 <Op verb="Task.claim" /> (Take this one), <Op verb="Task.return" /> with a reason, or <Op verb="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 <Op verb="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:** <Op verb="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 |
| --- | --- | --- | --- |
| <a id="check-owned-by-a-person"></a>`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. | <Op verb="Task.assign" /> |
| `mailbox-warm` (hub guard on <Op verb="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. | <Op verb="Mailbox.warm" /> |
| <a id="check-fact-check"></a>`fact-check` (hub guard on <Op verb="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. | <Op verb="Content.draft" /> |
| <a id="check-source-change"></a>`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. | <Op verb="Change.merge" /> |
| <a id="check-incident-hold"></a>`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. | <Op verb="Message.schedule" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-importer-test"></a>`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 |
| <a id="stop-data-loss"></a>`data-loss` | a commit differs from its dry run by over 0.5 % | the importer pauses; the Customer hears within 1 h |
| <a id="stop-bounce"></a>`bounce` | bounces pass 2 % of a day’s sends | <Op verb="Mailbox.rest" /> |
| <a id="stop-complaint"></a>`complaint` | complaints near 0.1 % | switch notes pause on every Mailbox |
| <a id="stop-reply-floor"></a>`reply-floor` | positive replies under 1 % after 300 sends | notes stop; the importer and pages keep running |
| <a id="stop-hostile-reply"></a>`hostile-reply` | 3 hostile replies in 30 days | You approve each one |
| <a id="stop-credit-limit"></a>`credit-limit` | switch credits reach the month’s Limit | grants pause; <Op verb="Approval.approve" /> for you |
| <a id="stop-fit"></a>`fit` | switchers’ 90-day retention 15 points under other new Customers | the switch Offer pauses |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | <Op verb="Process.pause" /> on `outbound`; the importer keeps running |
| + 3 more for the versus pages and rival-term ads (`?lanes=all`) | | |

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.

```json title="The captured refusal · sample"
{
  "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"
    }
  ]
}
```


```json title="The Stops that fired, as Campaign.paused reasons · sample"
[
  {
    "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 <Op verb="Metric.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`

| 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.

```json title="The Campaign card · sample"
{
  "$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=all`; 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: 0.2`) | 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 <Op verb="Task.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.

```diff title="Copperfield Homes, 2026-09-18 · the draft fact-check refused, then the rewrite that sent"
- 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
```mdx title="plays/switch-kit.mdx"
---
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.
```

```text
$ 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
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-rival-roster"></a>`rival-roster` | Rival roster admits the named rivals worth a switch kit, on searched demand, an export path and a fit verdict a sourced fact supports. Starts at You approve each one. |
| Tactic | <a id="tactic-rival-watch"></a>`rival-watch` | Rival watch captures each admitted rival’s public Pages daily, diffs them, holds every output a change affects and routes the change. Starts at Sends nothing. |
| Tactic | <a id="tactic-rival-fact-approval"></a>`rival-fact-approval` | Rival fact approval puts each new or changed claim about a named rival before one human, beside its capture, then releases the outputs citing it. Starts at You approve each one. |
| Tactic | <a id="tactic-importer-build"></a>`importer-build` | Importer build maps a rival’s export into the Startup’s Product, tests it against sample exports and launches it with a dated test report. Starts at You approve each one. |
| Tactic | <a id="tactic-importer-watch"></a>`importer-watch` | Importer watch runs every rival importer against its fixtures nightly and on export changes, and moves a failing one to assisted mode. Starts at It does it, you can stop it. |
| Tactic | <a id="tactic-switch-offer"></a>`switch-offer` | Switch offer grants a capped switching credit through Stripe to a new Customer whose first import comes from a named rival. Starts at Sends nothing. |
| Tactic | <a id="tactic-switch-window"></a>`switch-window` | Switch window turns a rival’s price rise, plan change, incident or backlash into 30 days of dated Pages, a pre-approved Offer, Ads and outreach order. Starts at Sends nothing. |
| Tactic | <a id="tactic-switch-outreach"></a>`switch-outreach` | Switch outreach reaches Organizations that announced they run a named rival, with the importer, the Offer and a dry-run ask, against a 20 percent holdout. Starts at You approve each one. |
| Tactic | <a id="tactic-compare-endpoint"></a>`compare-endpoint` | Compare endpoint answers a buying agent’s task with a fit verdict per option from approved facts, and runs an import dry run that commits nothing. Starts at Sends nothing. |
| Tactic | <a id="tactic-switch-readout"></a>`switch-readout` | Switch readout counts new Customers whose first import came from a named rival, by arm, against the Page split and the outreach holdout. Starts at Sends nothing. |
| Tactic | <a id="tactic-honest-compare"></a>`honest-compare` | Honest compare builds a comparison Page from captured rival sources and the claims source, with every row the rival wins and a stated method. Starts at You approve each one. |
| Tactic | <a id="tactic-editorial-publish-gate"></a>`editorial-publish-gate` | Editorial publish gate checks the copy rules, claims map, date, byline, duplicates, index rule and batch cap before a publish. Starts at Sends nothing. |
| Tactic | <a id="tactic-send-gate-check"></a>`send-gate-check` | Send gate check: Checks Suppression, a live reason, the Market, the basis stamp, the Person-month grant, the facts and the labels before any cold Message exists. Starts at Sends nothing. |
| Tactic | <a id="tactic-reply-matrix"></a>`reply-matrix` | Reply matrix: Labels every outreach reply with one of 11 labels, maps it to one of seven reply kinds, and runs that kind’s move by its deadline. Starts at You approve each one. |
| Tactic | <a id="tactic-search-account-build"></a>`search-account-build` | Search account build researches intent phrases and queues a complete, read-only account plan of ad groups, ads, negatives and breakeven bids. Starts at You approve each one. |
| Tactic | <a id="tactic-incrementality-holdout"></a>`incrementality-holdout` | Incrementality holdout measures the first Transactions a paid arm adds, by a geo holdout or a day switchback, before any scale step. Starts at You approve each one. |
| Trigger | <a id="trigger-rival-backlash"></a>`rival-backlash` | Rival backlash: `threshold` on `Monitor.checked`, window 14d. |
| Trigger | <a id="trigger-rival-user-announced"></a>`rival-user-announced` | Rival user announced: `event` on `Post.published`, window 30d. |
| Trigger | <a id="trigger-rival-export-change"></a>`rival-export-change` | Rival export change: `event` on `Monitor.checked`, window 1d. |
| Trigger | <a id="trigger-importer-test-failed"></a>`importer-test-failed` | Importer test failed: `event` on `Monitor.checked`, window 1d. |
| Trigger | <a id="trigger-import-dry-run-abandoned"></a>`import-dry-run-abandoned` | Import dry run abandoned: `state` on `File.uploaded`, window 72h. |
| Trigger | <a id="trigger-first-rival-import"></a>`first-rival-import` | First rival import: `event` on `Collection.imported`, window 30d. |
| Trigger | <a id="trigger-rival-alternative-named"></a>`rival-alternative-named` | Rival alternative named: `threshold` on `Monitor.checked`, window 90d. |
| Trigger | <a id="trigger-rival-page-change"></a>`rival-page-change` | Rival page change: `event` on `Monitor.checked`, window 72h. |
| Trigger | <a id="trigger-rival-moment"></a>`rival-moment` | Rival moment: `event` on `Organization.announced`, window 14d. |
| Trigger | <a id="trigger-rival-switch-ask"></a>`rival-switch-ask` | Rival switch ask: `event` on `Post.published`, window 48h. |
| Trigger | <a id="trigger-competitor-complaint"></a>`competitor-complaint` | Competitor complaint: `event` on `Review.published`, window 7d. |
| Trigger | <a id="trigger-technology-change"></a>`technology-change` | Technology change: `event` on `Monitor.checked`, window 30d. |
| Trigger | <a id="trigger-row-dispute"></a>`row-dispute` | Reading dispute: `event` on `Form.submitted`, window 2d. |
| Trigger | <a id="trigger-pricing-visit"></a>`pricing-visit` | Pricing visit: `event` on `Visit.recorded`, window 72h. |
| Sequence | <a id="sequence-switch-outreach-monthly"></a>`switch-outreach-monthly` | Switch outreach monthly: 4 steps; it ends on Any reply; A bounce; Suppression at any scope; A dry run or a signup; A booked Meeting; A missing Reason. |
| Sequence | <a id="sequence-switch-outreach-monthly-founder"></a>`switch-outreach-monthly-founder` | Switch outreach monthly founder: 4 steps; it ends on Any reply; A bounce; Suppression at any scope; A dry run or a signup; A booked Meeting; A missing Reason. |
| Sequence | <a id="sequence-switch-onboarding"></a>`switch-onboarding` | Switch onboarding: 6 steps; it ends on Cancel; Opt-out, except the charge reminder and receipts. |
| Sequence | <a id="sequence-dry-run-rescue"></a>`dry-run-rescue` | Dry run rescue: 2 steps; it ends on A commit; Any reply; Opt-out. |
| Benchmark | <a id="benchmark-switch-self-ranking-list-not-picked"></a>`switch-self-ranking-list-not-picked` | Share of AI-overview citations of a self-promotional best-software list that leave its owner out of the recommendation: 69 % (Search Engine Journal analysis of 100 B2B best-software queries, 2026-06-18). |
| Benchmark | <a id="benchmark-switch-comparison-content-influence"></a>`switch-comparison-content-influence` | Influence-score uplift for answer-engine-cited pages that carry comparison content, descriptive: 55 % (arXiv 2604.25707, a preprint on what shapes answer-engine responses, 2026-04-28). |