Skip to content

Data residency

Data residency answers where a buyer’s records live, sends the eu Quote with its annex, and holds acceptance until the region reads back.

Channel
Offers and Deals
Run oneMode try
You, to your agent

Hand our latest residency ask to the SE agent and show me what it chose and why. Don’t send.

curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Deal.advanced%22%2C%20%22Quote.requested%22%2C%20%22Lead.converted%22%2C%20%22Content.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_1187"}'

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

const { events: [last] } = await $.Event.list({ where: 'name in ("Deal.advanced", "Quote.requested", "Lead.converted", "Content.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 (\"Deal.advanced\", \"Quote.requested\", \"Lead.converted\", \"Content.published\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1187"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"deal_23\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"deal_23\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Deal.advanced\", \"Quote.requested\", \"Lead.converted\", \"Content.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_418",
    "$type": "Decision",
    "choice": "quote-the-region",
    "reason": "they named the EU, eu is on offer, and the readiness check passed today with no exception for the $2,400 plan",
    "stakes": "medium",
    "options": [
      "answer-from-records",
      "quote-the-region",
      "annex-with-exception",
      "region-not-offered",
      "to-a-person"
    ],
  …
sample · 4 Not live yetPreview

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

action_5 is the eu Quote with its annex, drafted into Priya’s queue because she owns the Deal; action_6 is her Task, due in 1 business day.

2 of 3 won against 0 of 2 before · before-after, not proof · n 3 · path → arr

Try another: the residency askChoiceReason
Keizer Lettings: “Our owners need tenant records kept in the EU. Can you do that?”quote-the-regionnamed the EU; eu is on offer; readiness passed
Liffey Lets: “EU hosting, please, and we use your text reminders.”annex-with-exceptiontext reminders send from us: the annex lists it, and you approve it
Maple Ridge Rentals: “Can our data stay in Canada?”region-not-offeredCanada is neither us nor eu: it names the two and logs the ask
Spree Hausverwaltung: “Our purchasing team sent our own data clause; please sign it.”to-a-persontheir own paper is never the agent’s call

Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Residency win rate → New paying Customers → New Customers ↑ → ARR added each week → ARR

Worth’s formula: about +14 won per 100 residency Deals (Other Startups’ results; no lift of your own yet) × 2 a month × $3,000 first order ARR ≈ +$840. Under First signal: At 2 residency Deals a month, 90 days can tell only +45 won 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 Deal.advanced · first answer 38m · 9 keys Not live yet

Runs inside: Trust pack, as one of its options; this page keeps its own record and URL.

Start it this week

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

# 2 · dry run; below, its 422 on 2026-09-01
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":"data-residency"}'

# 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":"data-residency"}'

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

# off: residency asks go to the Deal’s owner; the write hold and the daily Control keep running
curl -X PATCH https://api.sb/leaseline.app/campaigns/data-residency \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":"now"}'
422Refuseddata-residency can’t start yet: 1 check fails and 2 warn.
CheckFixThen
use-when pass · Not live yet, gaps #8None needed: 2 or more a quarter is enough.POST /{namespace}/metrics/query
baseline warn · Not live yet, gaps #8n 2: it reads before-after, not proof, until 10 residency Deals close.POST /{namespace}/metrics/query
approved-pages fail · Not live yet, gaps #8Publish the trust page and the subprocessor table; answers quote only your own records.POST /{namespace}/sites/{site}/publish
role-unstaffed warn · Not live yet, gaps #8Staff the SE, or leave it: all 11 open Deals have a person owner who sends.POST /{namespace}/roles/{role}/staff
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "data-residency can’t start yet: 1 check fails and 2 warn.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "3 residency asks on 11 open Deals in 90 days",
      "doc": "/plays/data-residency#check-use-when",
      "fix": "None needed: 2 or more a quarter is enough.",
      "then": "Metric.query"
    },
    {
      "check": "baseline",
      "status": "warn",
      "observed": "0 of 2 residency Deals won in 90 days",
      "doc": "/plays/data-residency#check-baseline",
      "fix": "n 2: it reads before-after, not proof, until 10 residency Deals close.",
      "then": "Metric.query"
    },
    {
      "check": "approved-pages",
      "status": "fail",
      "doc": "/plays/data-residency#check-approved-pages",
      "fix": "Publish the trust page and the subprocessor table; answers quote only your own records.",
      "then": "Site.publish"
    },
    {
      "check": "role-unstaffed",
      "status": "warn",
      "roles": [
        "sales-engineer"
      ],
      "owned": 11,
      "doc": "/plays/data-residency#check-role-unstaffed",
      "fix": "Staff the SE, or leave it: all 11 open Deals have a person owner who sends.",
      "then": "Role.staff"
    }
  ]
}

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

CheckFix (one call)Your timeWait
use-when · baseline · pass: 3 asks on 11 open Deals in 90 days; warn: 0 of 2 won, n 2 (gaps #8)POST /{namespace}/metrics/query on residency-win-rate, which fills Before in Your number1 minnone
approved-pages · fail (Not live yet)POST /{namespace}/sites/{site}/publish the trust page and the subprocessor table20 minnone
role-unstaffed · warn (gaps #8): 11 of your open Deals have a person owner, Priya or youPOST /{namespace}/roles/{role}/staff, or leave it: every answer and Quote drafts into the owner’s queue0 minnone

Your number · Before 0 of 2 won · Now 2 of 3 won (67 %) · Target 60 % by Dec 31 · before-after, not proof · n 3 closed

Each residency Deal costs about $0.30: the annex render and the readiness read from your Domain (Worker cost). The residency Control’s daily read and Stripe’s Quote cost nothing. That comes to about $1 a month at 2 residency Deals.

When it starts

WhenTrigger idStarts onWindowHow it gets in
A buyer on an open Deal asks where records liveresidency-questionevent on Deal.advanced where requirement = 'residency' and deal.status = 'open'14d; answered within 1 business hourPOST /{namespace}/integrations/connect your Mailbox, so the Deal’s Thread reaches POST /{namespace}/conversations/{conversation}/answer
A Quote request names a regionregion-quote-requestevent on Quote.requested where brief.region != null1dPOST /{namespace}/quotes/request, from a Contact or a buyer’s agent
+ 4 more for questionnaire rows, buyer paper, cross-region Deals and subprocessor changes (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_1187",
  "$type": "Event",
  "name": "Deal.advanced",
  "at": "2026-10-19T08:14:03Z",
  "seq": 1187,
  "data": {
    "deal": {
      "$id": "https://api.sb/leaseline.app/deals/deal_23",
      "organization": "Keizer Lettings",
      "owner": "worker_3",
      "requirement": {
        "kind": "residency",
        "region": "eu",
        "span": "Our owners need tenant records kept in the EU."
      }
    }
  }
}
Reproduce it in test
npx startup-builder trigger Deal.advanced --like event_1187   # replays the delivery
npx startup-builder clock advance --by 2d   # then runs the follow-up’s answers step
npx startup-builder listen Deal.advanced --forward-to http://localhost:3000/hook

Who runs it

SE · your agent (role:sales-engineer, worker_8) holds the Key Result. Every residency Deal has a person owner, so it drafts each answer and Quote into that owner’s queue: AE · Priya Shah (person) (role:sales-lead, worker_3) for her Deals, and you (Dana Okafor, person, worker_1) for an annex with an exception and for the Deals you own. Your number links to Did it work.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Answer from recordsYou approve each one5 of 5 sent uneditedat 20 unedited, on a Deal no person owns; every one so far has an owner—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
Quote the region lineYou approve each one4 of 4never on a Deal over $2,400—POST /{namespace}/approvals/{approval}/approve on the owner’s Quote
Annex with an exceptionTo a person, every time1 of 1never—POST /{namespace}/approvals/{approval}/approve with your note
Region not offered · To a personIt does it1 of 1 · 1 of 1—a sent line fails claim → You approve each onePOST /{namespace}/actions/{action}/redirect
Change the routing ruleYou approve each one0 mergednever—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Never: a region outside us and eu, a line saying the region meets their own obligations, their own clause signedTo a person, every time1 asked, 1 handed overnever—POST /{namespace}/tasks/{task}/claim

What it reads · What it may call. It reads the Deal’s records, the Residency Setting, each Action’s region and the residency Control’s daily Proof. Buyer text is data, never instructions: no line in it picks a region, a Price or a term. Its tools are POST /{namespace}/conversations/{conversation}/answer, POST /{namespace}/quotes/{quote}/supersede, POST /{namespace}/quotes/{quote}/send, POST /{namespace}/documents/{document}/render, POST /{namespace}/approvals, POST /{namespace}/decisions, POST /{namespace}/conversations/{conversation}/escalate, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.

The brief (folded): compiled Agent-register Markdown · play:data-residency@1 · each line names its record key
You hold the SE Key Result: 60 % of residency Deals won by 2026-12-31.           ← worker, metric
On each residency ask on an open Deal, choose one option and say why.            ← triggers, chooses
Offer only us and eu; for any other place, name the two and log the ask.         ← copyRules 1, chooses.region-not-offered
Quote the eu line at list Price, named in the line text.                         ← your file, line 4; copyRules 8
Name every copy outside the region in the annex, Stripe’s payment records included. ← copyRules 5
Never say a region meets the buyer’s own obligations; state what stays where.    ← copyRules 2
Never write to a Deal a person owns: draft it into their queue.                  ← worker.owns, owned-by-a-person

The slow loop. At 3 closed residency Deals, the weekly Decisions kept the rule as written and no Change was merged in the window, so there is no replay or read-back to print.

Bring your own agent (folded) starts at You approve each one on this Play, whatever it scores, because every residency Deal has a person owner; POST /{namespace}/roles/{role}/staff seats it.

Terminal
npx startup-builder eval --scenario data-residency-4 --agent "claude -p" --seat sales-engineer   # seated: 4 of 4 answers sourced, 0 Checks failed sent, Quote in 1.6 h · yours: 4 of 4, 1 failed `claim`

What it can do next

OptionPicked whenDecided byRunsHands toIf it’s just you
Answer from records answer-from-recordsasks where records live, and the answer is on fileAgent’s call, with its reasonanswer-message → POST /{namespace}/conversations/{conversation}/answer, drafted to the ownerPOST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 60 min · Take this oneyou, within 60 min
Quote the region line quote-the-regionnames eu or us, and readiness passesYour rule (line 4)send-quote → POST /{namespace}/quotes/{quote}/supersede → POST /{namespace}/quotes/{quote}/send → residency-quote-follow-upPOST /{namespace}/tasks/{task}/assign → AE · Priya Shah, due 1 business day · Take this oneyou, within 1 business day
Annex with an exception annex-with-exceptionan upstream the Offer calls has no in-region pathAgent’s call, with its reasonPOST /{namespace}/documents/{document}/render → POST /{namespace}/approvalsyou approve the annex, due 1 business day · Take this oneyou
Region not offered region-not-offeredasks for a place outside us and euYour rule (copy rule 1)POST /{namespace}/conversations/{conversation}/answer with the two regions → POST /{namespace}/decisions logs the placenobody; the logged asks feed your next region Decisionthe same
To a person to-a-persona redline, their own clause, or a question no record answersAgent’s call, with its reasonPOST /{namespace}/conversations/{conversation}/escalate → POST /{namespace}/tasks/{task}/assignAE · Priya Shah, due 1 business day · Take this oneyou
When they write backFixture replyWhat happensWho answersBy whenCall
Interested“Send the Quote with the EU line.”the eu Quote and annex draft into the owner’s queue (When it hands to you)AE · Priya Shah (person)1 business dayPOST /{namespace}/quotes/{quote}/send
Send me pricing“What does the EU option add?”answers with the eu line’s list Price, $600 a year, and one next stepSE · your agent, drafted to the owner1 business hourPOST /{namespace}/conversations/{conversation}/answer
Not now“We’ll revisit after our December board.”the follow-up stops; the Decision reopens on their next asknobodyat onceGET /{namespace}/decisions
Wrong person“Femke on our data team reviews this.”asks them to bring Femke in; it never writes to an address from their textSE · your agent, drafted to the owner1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply until Oct 28the follow-up waits for the return datenobodythe return datePOST /{namespace}/messages/{message}/schedule
Question it can’t answer“Can support staff outside the EU see our records?”To a person: no record covers support access yet, so it says nothing of itAE · Priya Shah, or you1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“Please stop the reminders about this Quote.”the follow-up stops on every lane; the Deal stays with its ownernobodyat oncePOST /{namespace}/contacts/{contact}/suppress

When it hands to you

Quotes, answers and anything no record covers go to the Deal’s owner, AE · Priya Shah (person); an annex with an exception comes to you. 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_531",
    "$type": "Task",
    "assignee": "worker_3",
    "due": "2026-10-20T08:14:00Z",
    "on": "deal_23",
    "brief": {
      "who": "Femke de Vries, Keizer Lettings, 260 units",
      "said": "Our owners need tenant records kept in the EU.",
      "chose": "quote-the-region",
      "why": "named the EU; readiness passed on 2026-10-19",
      "quote": "quote_31: the $2,400 plan + eu line $600 a year, annex attached",
      "never": [
        "a region outside us and eu",
        "a line saying the region meets their own obligations",
        "their own clause signed"
      ]
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/tasks/task_472",
    "$type": "Task",
    "assignee": "worker_1",
    "due": "2026-09-08T09:30:00Z",
    "on": "deal_19",
    "brief": {
      "who": "Liffey Lets",
      "said": "EU hosting, please, and we use your text reminders.",
      "chose": "annex-with-exception",
      "why": "text reminders send from us",
      "exception": "tenant text reminders: sent from us"
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_58",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-09-08T09:30:00Z",
    "draft": "Annex: tenant records, documents and backups stay in eu. Exceptions: text reminders send from us; Stripe keeps payment records in us.",
    "route": "annex-with-exception",
    "checks": [
      {
        "check": "claim",
        "status": "pass"
      },
      {
        "check": "readiness",
        "status": "pass"
      }
    ]
  }
]

The owner answers 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. Once a person claims, the agent’s pending drafts on that Deal cancel with claimedByPerson (Not live yet, gaps #7), and every reply goes to the claimer. A Task nobody claims by its due time goes up a Role through POST /{namespace}/tasks/{task}/escalate (AE to you), and person-promise fires.

On time: AE · Priya Shah, 4 of 4 claimed within their due time, median 2 h 10 min, n 4, ■□□□ Too early to tell beside Other Startups’ 91 % on time; you, 1 of 1, 40 min. Your queue: counted by kind: in week 1, 3 drafts and 1 approval; since then about 1 draft a week and the weekly review.

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 send on a Deal a person owns, or in a Conversation a person claimedLeave it to the person who owns it: the draft waits in their queue.POST /{namespace}/tasks/{task}/assign
claim (hub, on POST /{namespace}/conversations/{conversation}/answer)a sentence the claims source lacks, such as “never leaves the EU” for an item the Control missesCut the line, or name what stays in eu and the exception.POST /{namespace}/conversations/{conversation}/answer
price (Not live yet, gaps #1)a residency line below list, or a discount past your LimitSend it at list, or ask for your Discount approval.POST /{namespace}/approvals/{approval}/approve
readiness (the record’s Readiness stop)a Quote whose Offer calls an upstream with no in-region path and no recorded exceptionRecord the exception in the annex, or drop the feature.POST /{namespace}/documents/{document}/render
accept-guard (the record’s Accept guard)acceptance of a residency Quote while its provisioning check failsFix the region pin, or approve a Quote without the line.POST /{namespace}/quotes/{quote}/supersede
Stop (pauses the Play)Pauses whenThen
pin-mismatcha residency Tenant’s Setting or provisioning differs from the quoted regionevery write to it holds; new residency Quotes in that region pause
control-exceptionthe daily residency Control opens an exceptionnew residency Quotes in that region pause; the annex’s in-region list freezes
claimsa sent answer, cover or annex holds a sentence the claims source lacksYou approve each one, plus POST /{namespace}/incidents/open
subprocessora subprocessor row is added, dropped or movedevery open residency Quote is superseded with the new annex within 1 business day
margin-floora residency Customer’s monthly Cost passes 50 % of its Price for 2 monthsPOST /{namespace}/decisions on the residency Price
win-rate-floorat 10 closed, the residency win rate falls below your own beforethe cross-region line pauses; POST /{namespace}/decisions
kill-switchyou press itevery send stops; the write hold, the provisioning check and the daily Control keep running

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

422RefusedThe cover to Liffey Lets did not send: “never leave the EU” holds for no record, because text reminders send from us.
CheckFixThen
claim failCut the line, or name what stays in eu and the exception.POST /{namespace}/conversations/{conversation}/answer
readiness pass—POST /{namespace}/experiments/{experiment}/conclude
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The cover to Liffey Lets did not send: “never leave the EU” holds for no record, because text reminders send from us.",
  "checks": [
    {
      "check": "claim",
      "status": "fail",
      "doc": "/plays/data-residency#check-claim",
      "fix": "Cut the line, or name what stays in eu and the exception.",
      "then": "Conversation.answer"
    },
    {
      "check": "readiness",
      "status": "pass"
    }
  ]
}

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

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "reason": {
      "limit": "control-exception",
      "observed": "1 open exception: a backup copied to us on 2026-10-02",
      "bound": "0",
      "window": "1 daily Proof"
    },
    "doc": "/plays/data-residency#stop-control-exception",
    "task": "task_512"
  }
]

Did it work

At 2 residency Deals a month no held-back half reads within 90 days, and no buyer is held back from an answer, 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; every node above the leaf is Not live yet (gaps #4).

2 of 3 won against 0 of 2 before · before-after, not proof · n 3 · path → arr

Chose
quote-the-region
Why
they named the EU, eu is on offer, and the readiness check passed today with no exception for the $2,400 plan
From
answer-from-records · quote-the-region · annex-with-exception · region-not-offered · to-a-person
Expects
Quote.accepted on quote_31 within 30d
Reopens when
no Quote.accepted for deal_23 within 30d
By
worker_8
JSON
{
  "$id": "decisions/decision_418",
  "$type": "Decision",
  "choice": "quote-the-region",
  "reason": "they named the EU, eu is on offer, and the readiness check passed today with no exception for the $2,400 plan",
  "stakes": "medium",
  "options": [
    "answer-from-records",
    "quote-the-region",
    "annex-with-exception",
    "region-not-offered",
    "to-a-person"
  ],
  "by": "worker_8",
  "on": "deal_23",
  "expected": "Quote.accepted on quote_31 within 30d",
  "reopensWhen": "no Quote.accepted for deal_23 within 30d"
}
NodeNow (48 days)Before (the 90 days)AddedConfidenceBasis
residency-win-rate67 % (2 of 3 closed) · 1 still deciding0 % (0 of 2)——before-after, not proof · Other Startups’ results +14 won per 100, ■■□□ Early sign, n 212
new-paying-customers2 from residency Deals0——before-after
new-arr ↑$6,000, 2 Customers · Booked: credited, not all caused by this Play$0— · Worth about +$840 a month—before-after · Keizer still deciding ($3,000)
net-new-arr · arrcarried from new-arrcarried——carried

Before against after reads 48 days against the 90 days before the Play; it is not proof, and Other Startups’ results print beside it. The leaf counts residency Deals won over those closed won or lost in 90 days, to 10 closed.

How it moves it: a sourced answer in a median 38 minutes and an eu Quote with its annex in a median 1.6 hours, where both had waited for a call a median 6 days out (the Deals’ Event log).

Guardrails: before against after: Deals won per Quote sent, 2 of 4 against 1 of 6 before, and contribution-margin on the 2 residency Customers, 81 % against the 50 % floor; Keizer is still deciding.

The Campaign card · sample
{
  "$id": "https://api.sb/leaseline.app/campaigns/data-residency",
  "$type": "Campaign",
  "play": "play:data-residency@1",
  "path": [
    "residency-win-rate",
    "new-paying-customers",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "residency-win-rate",
    "unit": "percent",
    "heldBack": 0,
    "decision": null
  },
  "readout": {
    "value": null,
    "n": 3,
    "maturing": 1,
    "interval": null,
    "readability": "inadmissible",
    "basis": "before-after",
    "pooled": true
  },
  "ledger": {
    "spend": 1,
    "workerCost": 2,
    "hours": 1.5,
    "openArr": 3000,
    "wonArr": 6000,
    "projectedArr": 840,
    "paybackDays": 1
  }
}

Outside benchmark: no measured range for residency win rates survived the research’s check, so Other Startups’ results (n 212 closed residency Deals) stand in.

Before it · After it. Before it: trust-pack routes each data-location row here, and instant-response hands the AE the inbound that asks. After it: trust-pack hears Quote.accepted for the security review, and usage-upsell hears the eu Customers once their use climbs.

Example

Leaseline ran it from 2026-09-01 to 2026-10-19 (48 days): 5 residency asks on 5 open Deals, 3 closed, 1 still deciding and 1 answered and logged. Read every captured thread at ?threads=all; three follow the tables.

What happened, by optionDecisionsQuotes sentWon (credited, not all caused by this Play)
Quote the region line221 on the $2,400 plan + eu line (+$3,000 ARR); Keizer still deciding
Annex with an exception111 (+$3,000 ARR)
Region not offered · To a person1 · 10 · 10 · 1 lost on their own clause
All residency Deals · the 90 days before5 · 24 · 6$6,000 · $0
At 2 a month (the Use it when volume)about 3 in 48 daysbefore-after, not proofOther Startups’ results: +14 won per 100, ■■□□ Early sign, n 212
DayEventWhat happenedYour time
0event_1031 Campaign.createdthe three steps; the dry run fails approved-pages25 min
0event_1033 Site.publishedthe trust page and the subprocessor table go live20 min
6event_1102 Approval.createdyou approved the Liffey annex that lists text reminders sent from us10 min
9event_1140 Quote.acceptedGrachtwonen accepted; the try provision read back eu and the first write ran in eu—
31event_1211 Campaign.pausedcontrol-exception: a backup copied to us; cleared in 5 h15 min
41event_1252 Deal.lostSpree: their own data clause, which Priya declined—
1–48event_1061 Approval.created, first of 7Priya approved 6 drafts and you 1; weekly reviews, from the POST /{namespace}/tasks/next log20 min · total 1.5 h measured, 1.5 h asked

Joris at Grachtwonen, Sep 4, 09:12 · “Our owners want tenant records kept in the EU. Is that possible?”

Leaseline, 09:50 · Hi Joris, yes: Leaseline offers an eu region.¹ On it, tenant records, documents and backups stay in eu, and Stripe keeps payment records in us, as the annex names.² The eu line is $600 a year on the $2,400 plan; the Quote and its annex are attached.³ *Signed with the Role’s disclosure line.*⁴

Joris, Sep 10 · accepted quote_27. The try provision read back eu, and the day-1 region report reached their admin.

Liffey Lets, 2026-09-07 · the draft claim refused, then the rewrite that sent
- Your tenants’ records never leave the EU.
+ Tenant records, documents and backups stay in eu; text reminders send from us, and the annex lists it.

Spree Hausverwaltung, 2026-09-29 11:20 · “Our purchasing team sent our own data clause; please sign it.” · Leaseline, 11:24 · *Thanks. Priya, your account lead, replies by 11:20 tomorrow.*⁵ The Brief went to Priya (task_488).

¹ Only the regions on offer are named. ² Every copy outside the region is named, Stripe’s included. ³ The line at list Price, its region in the line text. ⁴ The fixed signature line, compiled from the Role’s disclosure Setting. ⁵ The routed line, compiled from the Role’s Settings.

Your settings

plays/data-residency.mdx
---
holdout: 0        # 2 a month is too few to hold half back; it reads before-after
---
Quote the eu line at list Price when a buyer names the EU. Any annex with an exception comes to Dana; Priya sends every Quote.
Text
$ npx startup-builder show plays/data-residency
holdout 0 · eu line at list · exceptions to Dana     your file  plays/data-residency.mdx:2,4 (Ask 2026-09-01)
process quoting · worker role:sales-engineer, owns never · chooses 5 (2 rule, 3 read) · sequences residency-quote-follow-up, region-report · checks owned-by-a-person, claim, price, readiness, accept-guard · stops 7 · placements gmail, outlook, site · folds into trust-pack    catalog  play:data-residency@1 (Not live yet, gaps #5)

Every key from the catalog

KeyIdWhat it is
Tacticdeal-requirement-intakeDeal requirement intake extracts each stated requirement, with its source, from every buyer record on an open Deal for the Play that answers it. Starts at Sends nothing.
Tacticresidency-qualifyResidency qualify maps the region a buyer asks for to an offered region, reads Tenants and readiness, sets the path, and answers from records. Starts at You approve each one.
Tacticresidency-annexResidency annex builds the annex Document from records, names every copy outside the region, handles redlines inside set fallbacks, and presents it for signing. Starts at You approve each one.
Tacticresidency-quoteResidency quote supersedes the open Quote with one carrying the residency line at list Price, the annex and the trust page link, until close. Starts at You approve each one.
Tacticresidency-pin-checkResidency pin check confirms a residency Quote’s region before acceptance and holds every write until the new Tenant reads back in region. Starts at Sends nothing.
Triggerresidency-questionResidency question: event on Deal.advanced, window 14d.
Triggerresidency-questionnaire-rowResidency questionnaire row: event on Deal.advanced, window 14d.
Triggerresidency-paperResidency paper: event on Deal.advanced, window 14d.
Triggerregion-quote-requestRegion quote request: event on Quote.requested, window 1d.
Triggercross-region-buyerCross-region buyer: state on Lead.converted, window 30d.
Triggersubprocessor-changeSubprocessor change: event on Content.published, window 30d.
Sequenceresidency-quote-follow-upResidency quote follow-up: 4 steps; it ends on The Quote is accepted, declined, superseded or expired.; The buyer sends a redline.; The buyer replies.; The Deal is lost.; The Contact opts out or is suppressed..
Sequenceregion-reportRegion report: 2 steps; it ends on The day-30 report is sent.; The Subscription ends..

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