Skip to content

Portfolio cross-sell

Shows your other Startup’s Offer inside your product, bill or email, labeled as a sister product, at the moment a Customer needs it.

Channel
Ads and sponsorships
Run oneMode try
You, to your agent

Hand Keyhold’s latest Customer with a renewal job 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(%22Event.captured%22%2C%20%22Subscription.unsubscribed%22%2C%20%22Lead.disqualified%22%2C%20%22Message.scheduled%22%2C%20%22Offer.published%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_1296"}'

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

const { events: [last] } = await $.Event.list({ where: 'name in ("Event.captured", "Subscription.unsubscribed", "Lead.disqualified", "Message.scheduled", "Offer.published")', 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 (\"Event.captured\", \"Subscription.unsubscribed\", \"Lead.disqualified\", \"Message.scheduled\", \"Offer.published\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1296"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"customer_388\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"customer_388\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Event.captured\", \"Subscription.unsubscribed\", \"Lead.disqualified\", \"Message.scheduled\", \"Offer.published\")', 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_671",
    "$type": "Decision",
    "choice": "sister-card",
    "reason": "Pine Street Rentals, a Keyhold Customer, exported 14 lease end dates to a spreadsheet on this screen; the need map gives that job to Leaseline, and they sit in the exposed 80 %",
    "stakes": "low",
    "options": [
      "sister-card",
      "sister-note",
      "after-the-cancel",
      "route-the-misfit",
      "not-now",
      "leave-it"
  …
sample · 4 Not live yetPreview

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

action_1 reads the holdout flag: Pine Street is in the exposed 80 %. action_2 records the card shown beside the export, labeled as a sister product, with Leaseline’s live Price.

+1.20 Leaseline Customers per 100 Keyhold Customers · ■□□□ Too early to tell · n 412 · path → arr

Try another: the host CustomerChoiceReason
Pine Street Rentals (Keyhold): exported 14 lease end dates to a spreadsheetsister-cardthe job on screen is one the need map gives Leaseline
Oak Lane Homes (Keyhold): cancelled, “we need renewal reminders, not deposits”after-the-cancelthe cancel completed first; the card follows it
A Leaseline Lead with 1 unit asking only about deposit trackingroute-the-misfitfits no Leaseline Offer; fits Keyhold; asks Yes or No thanks first
Birchwood Lets (Keyhold): at risk since Oct 5not-nowkeep-first: their CSM sees them first

Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Sibling acquisitions → Sent by your other Startups → New Customers ↑ → ARR added each week → ARR

Worth’s formula: about +1.20 Leaseline Customers per 100 Keyhold Customers a quarter × 412 ÷ 3 months × $348 first order ARR ≈ +$573. Under First signal: The 20 % of Keyhold Customers held back reads at 90 days and 500 Customers; at 412 it reads Too early to tell until more join.

sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Event.captured · first card at the next screen, 0.4 s · 9 keys Not live yet

Also runs as: House ads.

Start it this week

Terminal
npx startup-builder add play cross-sell   # 1 · writes plays/cross-sell.mdx (Not live yet, gaps #5)

# 2 · dry run; below, its 422 on 2026-07-21
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":"cross-sell"}'

# 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":"cross-sell"}'

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

# off: no card, slot or note renders; routed intros already sent stay
curl -X PATCH https://api.sb/leaseline.app/campaigns/cross-sell \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":"now"}'
422Refusedcross-sell can’t start yet: 1 check fails and 2 warn.
CheckFixThen
use-when pass · Not live yet, gaps #8Keyhold hosts with 412 active Customers; Leaseline has 38 paying from outside; Leaseline hosts misfit routing only (38, under 50)POST /{namespace}/metrics/query
billing-connected fail · Not live yet, gaps #8Connect Stripe on Keyhold so a sister Customer’s subscription reads live.POST /{namespace}/integrations/connect
baseline warn · Not live yet, gaps #8n 3: the held-back 20 % becomes the comparison at 90 days.POST /{namespace}/metrics/query
role-unstaffed warn · Not live yet, gaps #8Leave it: exclusion audits come to you; 9 Customers a person owns get drafts.POST /{namespace}/roles/{role}/staff
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "cross-sell can’t start yet: 1 check fails and 2 warn.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "Keyhold hosts with 412 active Customers; Leaseline has 38 paying from outside; Leaseline hosts misfit routing only (38, under 50)",
      "doc": "/plays/cross-sell#check-use-when",
      "then": "Metric.query"
    },
    {
      "check": "billing-connected",
      "status": "fail",
      "doc": "/plays/cross-sell#check-billing-connected",
      "fix": "Connect Stripe on Keyhold so a sister Customer’s subscription reads live.",
      "then": "Integration.connect"
    },
    {
      "check": "baseline",
      "status": "warn",
      "observed": "3 Leaseline Customers from Keyhold Customers in 90 days, unprompted",
      "doc": "/plays/cross-sell#check-baseline",
      "fix": "n 3: the held-back 20 % becomes the comparison at 90 days.",
      "then": "Metric.query"
    },
    {
      "check": "role-unstaffed",
      "status": "warn",
      "roles": [
        "privacy-officer"
      ],
      "owned": 9,
      "doc": "/plays/cross-sell#check-role-unstaffed",
      "fix": "Leave it: exclusion audits come to you; 9 Customers a person owns get drafts.",
      "then": "Role.staff"
    }
  ]
}

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

CheckFix (one call)Your timeWait
use-when · pass: Keyhold 412 active; Leaseline routes misfits only (gaps #8)POST /{namespace}/metrics/query on sibling-acquisitions1 minnone
billing-connected · failPOST /{namespace}/integrations/connect with system recurring-billing (Stripe) on Keyhold5 minthe first sync, about 10 min
baseline · warn: 3 unprompted in 90 daysPOST /{namespace}/metrics/query; it fills Before in Your number0 min90 days for the held-back read
role-unstaffed · warn (gaps #8): 9 Keyhold Customers have a person ownerPOST /{namespace}/roles/{role}/staff, or leave it: their cards draft into the owner’s queue0 minnone

Your number · Before 3 in 90 days, unprompted · Now +1.20 Leaseline Customers per 100 Keyhold Customers · Target +1.5 at the 500-Customer read · ■□□□ Too early to tell · n 412

Cards, receipt lines and newsletter slots ride Keyhold’s own surfaces at no cost, and a sister note costs $0.04 (Worker cost). Exclusion Audiences on each Startup’s Ads cost nothing. That comes to about $1 a month.

When it starts

WhenTrigger idStarts onWindowHow it gets in
A host Customer’s screen shows a job your sister Startup doesadjacent-needevent on Event.captured where a support tag, search, export or plan limit the need map gives the sister Startup14dKeyhold’s app calls POST /{namespace}/events/capture on the job
A host Customer cancels over a capability the sister hascancel-gapevent on Subscription.unsubscribed where the cancel or downgrade names a capability the sister’s live Offer delivers7dPOST /{namespace}/subscriptions/{subscription}/unsubscribe; the cancel completes first
The host schedules a newsletter issuehost-issue-scheduledevent on Message.scheduled where a host newsletter issue, which opens one sponsored slot2dPOST /{namespace}/messages/{message}/schedule on Keyhold’s newsletter
+ 3 more for misfit Leads, sister Offer changes and Studio sign-ins (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1296",
  "$type": "Event",
  "name": "Event.captured",
  "at": "2026-10-19T13:22:51Z",
  "seq": 1296,
  "data": {
    "startup": "keyhold",
    "customer": "customer_388",
    "name": "Pine Street Rentals",
    "job": "export lease end dates",
    "rows": 14,
    "needMap": "leaseline: renewal notices"
  }
}
Reproduce it in test
npx startup-builder trigger Event.captured --like event_1296   # replays the delivery
npx startup-builder clock advance --by 30d   # a second need after 30 days may show the card again
npx startup-builder listen Event.captured --forward-to http://localhost:3000/hook

Who runs it

CSM · your agent (role:success-manager, worker_5) holds the Key Result across the pair. Misfit Leads it routes go to Keyhold’s SDR · your agent after the Lead says yes, with the 5-minute answer, and a held Customer stays with their CSM. You (Dana Okafor, person, worker_1) own both Startups and approved the pair. Your number links to Did it work.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Show the sister card · Card after the cancelIt does it, you can stop it211 of 211 · 6 of 6met on day 14 (the first 20 reviewed)a render with no exclusion record, or dismissal fires → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Send the sister noteYou approve each one4 of 4at 20 approved—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Route the misfitIt does it9 asked, 5 yesthe fixed copy passed its replayconsent-stop → You approve each onePOST /{namespace}/actions/{action}/redirect
Not now · Leave itIt does it7 · 12——POST /{namespace}/actions/{action}/redirect
Change the pair or the need mapYou approve each one1 merged (a new need entry)never—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Never: a modal, a countdown, a card on a receipt email, a word about what they did elsewhereRefused, every time1 note refused (knowledge)never——

What it reads · What it may call. It reads the job on the Customer’s current screen, a need they stated in their own words, and the pair’s exclusion records; it never reads or names what a Person did at the other Startup. Its tools are POST /{namespace}/flags/{flag}/evaluate, POST /{namespace}/events/capture, POST /{namespace}/messages/{message}/schedule, and any other call becomes an Approval under the Role’s Mandate.

The brief (folded): compiled Agent-register Markdown · play:cross-sell@1 · each line names its record key
You hold the CSM Key Result: +1.5 Leaseline Customers per 100 Keyhold Customers at the 500-Customer read. ← worker, metric
Label every sister Offer with the sister label and Leaseline’s name, visible without a click. ← copyRules 1, 2
Name only the job on the current screen, or a need they stated themselves.       ← copyRules 4, 5
A cancel always completes first; the card never delays or argues with it.        ← copyRules 9
Ask a misfit Yes or No thanks, equally, before anything passes to Keyhold.       ← copyRules 12, consent-stop

The slow loop, as a Change. change_19 (Aug 30) added “export lease end dates” to the need map after 9 Pine-Street-like exports in a week; you merged it. Replayed on the past 30 days it would have shown 31 more cards; read back at +30d, 2 of 31 subscribed, n 31, ■□□□ Too early to tell.

What it can do next

OptionPicked whenDecided byRunsHands toIf it’s just you
Show the sister card sister-cardthe job on screen is one the need map gives the sisterYour rule (the need map, line 4)POST /{namespace}/flags/{flag}/evaluate the holdout → POST /{namespace}/events/capture the card shownthe Customer’s own clickthe same
Send the sister note sister-notea need stated in their own words, in a Keyhold emailAgent’s call, with its reasonsister-note, once, from Keyholdyou approve it · Take this onethe same
Card after the cancel after-the-cancela cancel over a capability Leaseline hasYour rule (copyRules 9)POST /{namespace}/flags/{flag}/evaluate, after the cancel completesnobodythe same
Route the misfit route-the-misfita Lead that fits no Leaseline Offer and fits KeyholdAgent’s call, with its reasonPOST /{namespace}/messages/{message}/schedule the Yes or No thanks ask → on yes, misfit-introKeyhold’s SDR · your agent, due 5 min after yesyou
Not now not-nowanother Play holds them, or keep-first appliesYour rule (rule j)nothing; names the holdertheir CSM · Take this oneyou
Leave it leave-itthey chose Hide sister products, or no exclusion recordYour rule (the Exclusion audit)nothing; nothing rendersnobodythe same
When they write backFixture replyWhat happensWho answersBy whenCall
Interested“Yes, introduce us to Keyhold.”Keyhold’s first Message names Leaseline as referrer in its first line, within 5 minutesKeyhold’s SDR · your agent5 minPOST /{namespace}/leads/{lead}/assign
Send me pricing“What does Leaseline cost on top of Keyhold?”Leaseline’s list Price from its Pricing Page, with the sister labelCSM · your agent1 business hourPOST /{namespace}/conversations/{conversation}/answer
Not now“Maybe after renewal season.”no card or note for 30 days; customer-ask-cap frees themnobodyat onceGET /{namespace}/decisions
Wrong person“My partner handles the leases.”asks them to forward it; it never writes to an address from their textCSM · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply until Oct 28the note waits for the return datenobodythe return datePOST /{namespace}/messages/{message}/schedule
Question it can’t answer“Can you bundle both for one price?”To a person: a bundle price is never the agent’s callyou1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stoppressed Hide sister productsno sister Offer shows to them again, on any surfacenobodyat oncePOST /{namespace}/contacts/{contact}/unsubscribe

When it hands to you

Routed misfits go to Keyhold’s SDR · your agent after the Lead says yes; a held Customer stays with their CSM, and sister notes and bundle questions come to you (Dana Okafor, 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_681",
    "$type": "Task",
    "assignee": "worker_12",
    "due": "2026-10-08T15:05:00Z",
    "on": "lead_47",
    "brief": {
      "who": "Marta Diaz, 1 unit, asked Leaseline about deposit tracking",
      "said": "I just need to track one deposit and its return date",
      "chose": "route-the-misfit",
      "why": "fits no Leaseline Offer; fits Keyhold’s free tier",
      "consent": "consent_77, naming Keyhold",
      "firstLine": "names Leaseline as referrer"
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/tasks/task_676",
    "$type": "Task",
    "assignee": "worker_5",
    "due": "2026-10-06T17:00:00Z",
    "on": "customer_401",
    "brief": {
      "who": "Birchwood Lets, a Keyhold Customer",
      "chose": "not-now",
      "why": "at risk since Oct 5: 2 failed payments",
      "held": "keep-first"
    }
  }
]

Each 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. Once a person claims, no card or note renders for that Customer, and every reply goes to the claimer. A routed Lead unanswered in 5 minutes counts toward routing, and a Task nobody claims by its due time goes up through POST /{namespace}/tasks/{task}/escalate.

On time: Keyhold’s SDR · your agent, 5 of 5 answered within 5 minutes, median 1m 40s; CSM, 7 of 7; n 12, ■□□□ Too early to tell beside Other Startups’ 96 % on time. Your queue: counted by kind: about 1 sister note to approve a week and 1 bundle question a month.

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; Not live yet, gaps #8)a card or note to a Customer whose open Deal or Conversation a person ownsLeave 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 sister Offer to a Customer who is at risk, past due or leavingLet 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 Playusage-upsell holds them until Nov 3.GET /{namespace}/decisions
consent-stop (gaps #3)an intro to Keyhold the Lead has not said yes toAsk once for a yes to the other Startup.POST /{namespace}/messages/{message}/schedule
knowledge (the record’s Knowledge hold)a card, slot, note or intro that refers to what a Person did at the other StartupName only the job on screen, or their own words.POST /{namespace}/content/draft
Stop (pauses the Play)Pauses whenThen
exclusion-auditany render or Message with no exclusion recordevery step pauses at once
host-churnat 90 days, exposed host churn over the held back by 1 per 100the pair’s sister Offers pause
dismissalHide sister products chosen by over 3 % of those shown, over 300 in 30 daysthe pair pauses
complaintspam complaints near 0.1 % of a day’s slots or notessister email pauses on every host Mailbox
opt-outissues with a slot drawing unsubscribes 0.5 points above those without, over 4 issuesthe slot drops
yield-floorthe acquisitions lift less the host churn lift at or under 0 at 90 daysthe pair’s sister Offers end; a Decision opens
kill-switchyou press itevery step pauses on every pair
+ 1 more for the routing lane: a sister that misses the 5-minute answer (?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 Cedar Mews thread in Example.

422RefusedThe sister note to Cedar Mews did not send: it refers to what they did at Keyhold’s deposit screen.
CheckFixThen
knowledge failName only the job on screen, or their own words.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 sister note to Cedar Mews did not send: it refers to what they did at Keyhold’s deposit screen.",
  "checks": [
    {
      "check": "knowledge",
      "status": "fail",
      "doc": "/plays/cross-sell#check-knowledge",
      "fix": "Name only the job on screen, or their own words.",
      "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": "exclusion-audit",
      "observed": "1 render with no exclusion record",
      "bound": "0",
      "window": "1 render"
    },
    "doc": "/plays/cross-sell#stop-exclusion-audit",
    "task": "task_633"
  }
]

Did it work

Keyhold’s Customers were split at random, 80 % exposed and 20 % held back for 90 days, so both halves read from the same assignment (rules m–o). The read reaches its minimum at 500 Customers, so it prints Too early to tell with its counts. Each figure’s call is POST /{namespace}/metrics/query by node, and every node above the leaf is Not live yet (gaps #4).

+1.20 Leaseline Customers per 100 Keyhold Customers · ■□□□ Too early to tell · n 412 · path → arr

Chose
sister-card
Why
Pine Street Rentals, a Keyhold Customer, exported 14 lease end dates to a spreadsheet on this screen; the need map gives that job to Leaseline, and they sit in the exposed 80 %
From
sister-card · sister-note · after-the-cancel · route-the-misfit · not-now · leave-it
Expects
Offer.subscribed at Leaseline within 90d
Reopens when
the Customer chooses Hide sister products, or a second adjacent need after 30d
By
worker_5
JSON
{
  "$id": "decisions/decision_671",
  "$type": "Decision",
  "choice": "sister-card",
  "reason": "Pine Street Rentals, a Keyhold Customer, exported 14 lease end dates to a spreadsheet on this screen; the need map gives that job to Leaseline, and they sit in the exposed 80 %",
  "stakes": "low",
  "options": [
    "sister-card",
    "sister-note",
    "after-the-cancel",
    "route-the-misfit",
    "not-now",
    "leave-it"
  ],
  "by": "worker_5",
  "on": "customer_388",
  "expected": "Offer.subscribed at Leaseline within 90d",
  "reopensWhen": "the Customer chooses Hide sister products, or a second adjacent need after 30d"
}
NodeExposed (330)Held back (82)AddedConfidenceBasis
sibling-acquisitions2.42 Leaseline Customers per 100 (8 of 330)1.22 (1 of 82)+1.20 [−1.6, +4.0]■□□□ Too early to tellheld back at random · n 412 of the 500 minimum · Other Startups’ results +0.9 per 100, ■■■□ Likely, n 9,400
sibling-sourced-share ↑8 Leaseline Customers sent by Keyhold4 expected (1 × 4)+4 [−3, +11] · Not carrying up yet■□□□ Too early to tellthe same assignment
new-arr ↑$2,784, 8 Customers · Booked $3,132: credited, not all caused by this Play (1 routed misfit at Keyhold)$1,392, 4 expected+$1,392 [−$2,400, +$5,200] · Worth about +$570 a month■□□□ Too early to tell (8 vs 4)the same · 3 trials still deciding
net-new-arr · arr+$2,784+$1,392+$1,392, carried■□□□ Too early to tellcarried from new-arr

The leaf counts Leaseline Customers won per 100 exposed Keyhold Customers over 90 days, less the rate in the held-back Customers, whoever reached them; misfit routing reads one host-week in 5 held back, separately.

How it moves it: a labeled card beside the job on screen, once, with Leaseline’s live Price; 211 cards shown, 8 subscribed.

Guardrails: treated against held back: Keyhold’s own churn (churned-arr), 2.1 against 1.6 per 100, inside the 1-point Stop, and Hide sister products, 1.4 % of those shown.

The Campaign card · sample
{
  "$id": "https://api.sb/leaseline.app/campaigns/cross-sell",
  "$type": "Campaign",
  "play": "play:cross-sell@1",
  "path": [
    "sibling-acquisitions",
    "sibling-sourced-share",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "sibling-acquisitions",
    "unit": "per-100",
    "heldBack": 82,
    "decision": null
  },
  "readout": {
    "value": 1.2,
    "n": 412,
    "maturing": 0,
    "interval": [
      -1.6,
      4
    ],
    "readability": "D",
    "basis": "held-back",
    "pooled": false
  },
  "ledger": {
    "spend": 0,
    "workerCost": 1,
    "hours": 1.9,
    "pipelineArr": 1044,
    "wonArr": 3132,
    "projectedArr": 573,
    "paybackDays": 0
  }
}

Outside benchmark: none recorded for this Play yet; Other Startups’ results print beside every row.

Before it · After it. Before it: instant-response grades each Lead’s fit and hands misfits here (Lead.disqualified), and owned-newsletter opens the slot. After it: usage-upsell and customer-referrals take the sister Customers once they’re active; each readout seeds the next pair’s estimate. house-ads runs inside it.

Example

Leaseline ran it from 2026-07-21 to 2026-10-19 (90 days) on one pair: Keyhold (412 active Customers, 330 exposed and 82 held back) hosted Leaseline’s sister Offer, and Leaseline routed 9 misfit Leads to Keyhold. Read every captured thread at ?threads=all; three follow the tables.

What happened, by optionDecisionsShown or sentCustomers (Booked: credited, not all caused by this Play)
Show the sister card · Card after the cancel211 · 6211 cards · 6 after cancels7 · 1
Send the sister note44 notes, 1 rewritten after knowledge0
Route the misfit99 asked, 5 yes, 5 intros in 5 min1 at Keyhold
Not now · Leave it7 · 12——
At 50 host Customers (the Use it when volume)40 exposed, 10 held backreads after about 3 years of 90-day windowsOther Startups’ results: +0.9 per 100, ■■■□ Likely, n 9,400
DayEventWhat happenedYour time
0event_610 Campaign.createdthe three steps; the dry run fails billing-connected on Keyhold20 min
0event_611 Integration.connectedStripe on Keyhold; you approve the pair and its need map15 min
6event_688 Offer.subscribedthe first Leaseline Customer from a Keyhold card—
14event_740 Role.updatedpromoted: cards render after a 2-minute hold—
23event_801 Campaign.pausedexclusion-audit: one render had no exclusion record; fixed and resumed in 2 h15 min
40event_912 Change.mergedchange_19: “export lease end dates” joins the need map5 min
62event_1101 Action.refusedknowledge refused a sister note to Cedar Mews; the rewrite sent—
1–90event_640 Approval.created, first of 4you approved 4 notes; weekly reviews, from the POST /{namespace}/tasks/next log40 min · total 1.9 h measured, 2 h asked

Keyhold, beside the export button, Oct 19, 13:22 · *Sister product, same owner: Leaseline.*¹ Tracking lease end dates? Leaseline sends each renewal notice 60 days out, per owner: $29 a month.² Try Leaseline · Hide sister products³

Pine Street Rentals, 13:31 · opened Leaseline’s trial from the card (Offer.trialed).

Cedar Mews, 2026-09-21 · the sister note knowledge refused, then the rewrite that sent
- We saw you export your deposit dates every Monday; Leaseline can do the renewals too.
+ You asked about renewal reminders last week. Leaseline, our sister product, sends each notice 60 days out: $29 a month.

Marta Diaz, to Leaseline, 2026-10-08 15:00 · “I just need to track one deposit and when it’s due back.” · SDR · your agent, 15:00 · asked Yes or No thanks to an intro to Keyhold, our sister product. · Marta, 15:02 · Yes. The Brief went to Keyhold’s SDR agent (task_681), due 15:05; its first Message, naming Leaseline as referrer, went at 15:03.

¹ The sister label and the receiver’s name, visible without a click. ² The job on the current screen and the live Price from Leaseline’s Pricing Page. ³ Not a modal; it never covers the host’s own action.

Your settings

plays/cross-sell.mdx
---
pair: keyhold → leaseline
holdout: 0.2      # 1 in 5 Keyhold Customers sees no sister Offer for 90 days
needs: [export lease end dates, renewal reminders]
---
Show Leaseline once, labeled as our sister product, beside a renewal job in Keyhold. Route Leaseline’s misfit Leads to Keyhold only on a yes.
Text
$ npx startup-builder show plays/cross-sell
pair keyhold → leaseline · holdout 0.2 · needs 2     your file  plays/cross-sell.mdx:2-4,6 (Ask 2026-07-21; need by change_19, 2026-08-30)
family referral · also runs house-ads · worker role:success-manager, owns never · chooses 6 (4 rule, 2 read) · sequences misfit-intro, sister-note · checks owned-by-a-person, keep-first, customer-ask-cap, consent-stop, knowledge · stops 8 · placements portfolio-network, gmail, outlook, yahoo-mail, facebook, linkedin, google-search, google-display (exclusions)    catalog  play:cross-sell@1 (Not live yet, gaps #5)

Every key from the catalog

KeyIdWhat it is
Tacticsibling-pairingSibling pairing scores ICP overlap between Studio-held siblings each week and writes, for each kept pair, the host jobs the receiver’s Offer serves. Starts at You approve the first wave.
Tacticsibling-exclusionSibling exclusion decides, before every sibling render, Message and Audience sync, whether a Person may see a receiver’s Offer, and logs the reason. Starts at Sends nothing.
Tacticsister-offerSister offer shows one labeled, dismissible card for a paired sibling in the host’s Product or hosted Invoice Page when a need shows. Starts at You approve each one.
Tacticmisfit-routingMisfit routing answers a Lead the host cannot serve, asks once to introduce the fitting sibling, and on a yes hands over the Lead. Starts at Sends nothing.
Tacticstudio-sign-inStudio sign-in offers one visible Studio account at a sibling’s sign-up and checkout, only on Studio Brands, and carries identity and wallet across. Starts at Sends nothing.
Tacticsister-slotSister slot places one labeled sibling Offer in the host’s newsletter issue and one host-signed sister note per Person per pair per 90 days. Starts at You approve each one.
Tacticcross-sell-readoutCross-sell readout nets each pair’s receiver Customers against host churn, both against the holdout, and pauses pairs that cost more than they win. Starts at Sends nothing.
Triggeradjacent-needAdjacent need: event on Event.captured, window 14d.
Triggercancel-gapCancel gap: event on Subscription.unsubscribed, window 7d.
Triggermisfit-leadMisfit lead: event on Lead.disqualified, window 1d.
Triggerhost-issue-scheduledHost issue scheduled: event on Message.scheduled, window 2d.
Triggersibling-offer-changeSibling offer change: event on Offer.published, window 7d.
Triggerstudio-sign-in-startStudio sign-in start: event on Event.captured, window 1h.
Sequencemisfit-introMisfit intro: 3 steps; it ends on The Person replies.; The Person books a Meeting.; The Person signs up at the receiver.; The Person withdraws Consent or is suppressed.; 7 days pass from the intro..
Sequencesister-noteSister note: 1 steps; it ends on The note is sent.; The Person is suppressed or unsubscribes before the send..

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