Skip to content

Programmatic SEO

Turns each record only you hold into one search Page that leads with its fact, released in batches that stop when search ignores them.

Channel
Blog and search
Kind
Search
Run oneMode try
You, to your agent

Hand our latest batch at its 28-day read to the Content Lead agent and show me what it chose and why. Don’t publish.

curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Keyword.measured%22%2C%20%22Collection.filled%22%2C%20%22Site.published%22%2C%20%22Page.unindexed%22%2C%20%22Metric.watched%22%2C%20%22Fact.changed%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_3150"}'

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

const { events: [last] } = await $.Event.list({ where: 'name in ("Keyword.measured", "Collection.filled", "Site.published", "Page.unindexed", "Metric.watched", "Fact.changed")', 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 (\"Keyword.measured\", \"Collection.filled\", \"Site.published\", \"Page.unindexed\", \"Metric.watched\", \"Fact.changed\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_3150"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"collection_7\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"collection_7\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "const { events: [last] } = await $.Event.list({ where: 'name in (\"Keyword.measured\", \"Collection.filled\", \"Site.published\", \"Page.unindexed\", \"Metric.watched\", \"Fact.changed\")', 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_455",
    "$type": "Decision",
    "choice": "grow-the-batch",
    "reason": "52 of batch 3’s 60 Pages are indexed at day 28 (87 %), at 3.4 visits each; 120 more stays under the 400-Page cap",
    "stakes": "medium",
    "options": [
      "release-batch",
      "grow-the-batch",
      "hold-for-rework",
      "recheck-fact",
      "withdraw-batch",
      "to-the-marketing-lead"
  …
sample · 4 Not live yetPreview

On POST /{namespace}/processes/{process}/enableOff POST /{namespace}/processes/{process}/pauseTake this one POST /{namespace}/tasks/{task}/claim

action_3 releases batch 4, 120 renewal-rate Pages, once you approve it: the option’s stakes are medium. Each Page links the method Page with its lease count and the date checked.

+2.8 visits per indexed Page · ■■■□ Likely · n 240 · path → arr

Try another: the batchChoiceReason
Batch 3, 60 renewal-rate Pages: 52 indexed at day 28grow-the-batch87 % indexed, 3.4 visits each
Dallas Page: its 2026 renewal share moved from 68 % to 71 %recheck-factthe lead fact changed; the date moves with it
Turnover Pages, batch 1: 18 of 40 left out of the index at day 28withdraw-batch45 % left out passes the 40 % stop
Eugene Page draft: its lead fact matches a public housing reporthold-for-reworka fact another source states is no lead

Where it sits: Stranger → Lead → Trialing → Activated → At risk → Churned → Won back · Indexed Page yield → People reached → New Customers ↑ → ARR added each week → ARR

Worth’s formula: about +2.8 visits per indexed Page × 130 Pages a month × 0.03 visits to signup × 0.12 signups to paid (your last 90 days) × $347 Pro ARR ≈ +$455. Under First signal: About 5 weeks to tell a +2 lift at 130 Pages a month (1 to read, 4 for the 28-day window); at 30 Pages, about 9 weeks.

sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Site.published · first Page indexed on day 6 · 7 keys Not live yet

Start it this week

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

# 3 · keep it, at You approve the first wave
curl -X POST https://api.sb/leaseline.app/campaigns \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"play":"programmatic-seo"}'

# on
curl -X POST https://api.sb/leaseline.app/processes/content-marketing/enable \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"

# off: no batch releases; released Pages stay up
curl -X POST https://api.sb/leaseline.app/processes/content-marketing/pause \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
422Refusedprogrammatic-seo can’t start yet: 1 check fails and 2 warn.
CheckFixThen
use-when pass · Not live yet, gaps #8Keep 30 or more records with a fact of their own, or run editorial-seo.POST /{namespace}/metrics/query
baseline pass · Not live yet, gaps #8Read it once; it fills Before in Your number.POST /{namespace}/metrics/query
approved-pages fail · Not live yet, gaps #8Publish the method Page every city Page links to.POST /{namespace}/sites/{site}/publish
fact-check warn · Not live yet, gaps #1Those 19 stay held until each reaches 30 leases.POST /{namespace}/content/draft
role-unstaffed warn · Not live yet, gaps #8Staff the Marketing Lead, or leave it: incidents and approvals come to you.POST /{namespace}/roles/{role}/staff
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "programmatic-seo can’t start yet: 1 check fails and 2 warn.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "62 cities with 30 or more leases; demand for the query in 58",
      "doc": "/plays/programmatic-seo#check-use-when",
      "fix": "Keep 30 or more records with a fact of their own, or run editorial-seo.",
      "then": "Metric.query"
    },
    {
      "check": "baseline",
      "status": "pass",
      "observed": "0.4 search visits per city a month to your existing Pages, n 120",
      "doc": "/plays/programmatic-seo#check-baseline",
      "fix": "Read it once; it fills Before in Your number.",
      "then": "Metric.query"
    },
    {
      "check": "approved-pages",
      "status": "fail",
      "observed": "no method Page: Dataset, lease count, date checked",
      "doc": "/plays/programmatic-seo#check-approved-pages",
      "fix": "Publish the method Page every city Page links to.",
      "then": "Site.publish"
    },
    {
      "check": "fact-check",
      "status": "warn",
      "observed": "19 of 62 cities hold under 30 leases",
      "doc": "/plays/programmatic-seo#check-fact-check",
      "fix": "Those 19 stay held until each reaches 30 leases.",
      "then": "Content.draft"
    },
    {
      "check": "role-unstaffed",
      "status": "warn",
      "roles": [
        "marketing-lead"
      ],
      "owned": 0,
      "doc": "/plays/programmatic-seo#check-role-unstaffed",
      "fix": "Staff the Marketing Lead, or leave it: incidents and approvals come to you.",
      "then": "Role.staff"
    }
  ]
}

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

CheckFix (one call)Your timeWait
use-when · baseline · pass: 62 cities, demand in 58; 0.4 visits per city a month, n 120 (gaps #8)POST /{namespace}/keywords/{keyword}/measure on the pattern, then POST /{namespace}/metrics/query on indexed-page-yield, which fills Before2 minnone
approved-pages · fail: no method PagePOST /{namespace}/sites/{site}/publish the method Page15 minnone
fact-check · warn: 19 cities under 30 leasesPOST /{namespace}/content/draft holds them; they join a batch at 300 minuntil each city reaches 30 leases
role-unstaffed · warn (gaps #8): no Marketing LeadPOST /{namespace}/roles/{role}/staff, or leave it: incidents and approvals come to you0 minnone

Your number · Before 0.4 · Now 3.2 visits per indexed Page · Target 3.0 by Dec 31 · ■■■□ Likely · n 240 cities

Each Page costs $0.02 to draft and fact-check (Worker cost), and POST /{namespace}/keywords/{keyword}/measure reads cost $0.01 a pattern. Hosting on your own Site costs nothing. That comes to about $2.60 a month at 130 Pages and $0.60 at 30.

When it starts

WhenTrigger idStarts onWindowHow it gets in
A city reaches 30 complete lease recordsrecord-set-readythreshold on Collection.filled where complete records >= the Page kind's minimuminto the next batch within 30dyour records sync through POST /{namespace}/integrations/{integration}/sync
A released batch reaches day 28batch-window-closedat on Site.published where 28 days after the batch's Site.publisheddecide within 7dthe clock, from the batch’s POST /{namespace}/sites/{site}/publish
Search fetched a Page and left it outcrawled-not-indexedstate on Page.unindexed where status = 'unindexed' after a fetch14dPOST /{namespace}/integrations/connect with your search console
+ 4 more for query demand, index drops, changed facts and silent Pages (?lanes=all)
One signed delivery · StartupBuilder-Signature
{
  "$id": "https://api.sb/leaseline.app/events/event_3150",
  "$type": "Event",
  "name": "Site.published",
  "at": "2026-09-21T09:00:00Z",
  "seq": 3150,
  "data": {
    "site": "site_1",
    "collection": "collection_7",
    "batch": 3,
    "pages": 60,
    "kind": "renewal-rate-by-city",
    "readAt": "2026-10-19"
  }
}
Reproduce it in test
npx startup-builder trigger Site.published --like event_3150   # replays the release
npx startup-builder clock advance --by 28d   # fires batch-window-closed on day 28
npx startup-builder listen Site.published --forward-to http://localhost:3000/hook

Who runs it

Content Lead · your agent (role:content-lead, worker_6) holds the Key Result. Growing a batch goes to you (Dana Okafor, person, worker_1) as an Approval, and incidents go to the Marketing Lead Role, which is unstaffed, so you. It writes no Message to anyone. Your number links to Did it work.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Release the batch · Recheck and redateIt does it, you can stop it3 of 3 · 6 of 6met after batch 2 (both read clean at day 28)any Page fails fact-check after release → You approve each onePOST /{namespace}/actions/{action}/cancel before the release
Hold for rework · Withdraw the batchIt does it44 of 44 · 1 of 1——POST /{namespace}/actions/{action}/redirect
Grow the next batchYou approve each one2 of 2never: its stakes are medium—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse
To the Marketing LeadIt does it1 of 1——POST /{namespace}/tasks/{task}/claim
Change the Page kind’s TemplateYou approve each one1 merged of 1never—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Never: a rating no Customer gave, a fact from another source, a made-up authorRefused, every time3 drafts refusednever—nothing; fact-check holds it

What it reads · What it may call. It reads your lease records, the query pattern’s demand and each Page’s index status. A Page states only what its record holds, with the lease count and the date checked. Its tools are POST /{namespace}/sites/{site}/publish, POST /{namespace}/content/draft, Content.withdraw (Not live yet, gaps #2), POST /{namespace}/issues/{issue}/triage, POST /{namespace}/incidents/open, 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:programmatic-seo@1 · each line names its record key
You hold the Content Lead Key Result: 3.0 visits per indexed Page by 2026-12-31. ← worker, metric
On each Trigger, choose one option and say why.                                  ← triggers, chooses
One Page per city with 30 or more leases, leading with its 2026 renewal share.   ← your file, line 4
State the lead fact in the first 100 words, with the method line.                ← copyRules 1, 4
The updated date moves only when a fact on the Page changes.                     ← copyRules 5
No rating markup, no generated author, no fact another source states.            ← copyRules 3, 12; fact-check

The slow loop, as a Change. decision_391 (2026-09-14, weekly) read that batch 1’s Pages with the lease count in the title indexed at 90 % against 70 % without it, so change_27 moved the count into every title (the Template) and Dana merged it with POST /{namespace}/changes/{change}/merge. Replayed on past batches, POST /{namespace}/changes/{change}/review put the indexed share at 86 %; read back on batch 3 at day 28 it is 87 %, n 60 (Example, day 56).

Bring your own agent (folded) starts at It does it, you can stop it when it matches or beats the seated agent on the eval suite, and at You approve each one when it does not; POST /{namespace}/roles/{role}/staff seats it.

Terminal
npx startup-builder eval --scenario programmatic-seo-240 --agent "claude -p" --seat content-lead   # seated: 3.2 visits per Page, 0 fact-check fails published, $0.02 a Page · yours: 2.9, 2 fails, $0.03 → You approve each one

What it can do next

OptionPicked whenDecided byRunsHands toIf it’s just you
Release the batch release-batchcertified Pages pass the fact gate, under the capYour rule (line 5)publish-content → POST /{namespace}/sites/{site}/publishnobodythe same
Grow the next batch grow-the-batchthe last batch indexed 75 % or more by day 28Agent’s call, with its reasonPOST /{namespace}/sites/{site}/publish on the next, larger batchyou approve it, due 1 business day · Take this oneyou
Hold for rework hold-for-reworka lead fact missing or stale, or a Page that reads like anotherAgent’s call, with its reasonPOST /{namespace}/content/draft back to heldnobodythe same
Recheck and redate recheck-factthe fact a Page leads with changedYour rule (copy rule 5)POST /{namespace}/content/draft → POST /{namespace}/sites/{site}/publish, with the new datenobodythe same
Withdraw the batch withdraw-batchsearch leaves 40 % or more out at the window’s closeYour rule (the index stall Stop)Content.withdraw (Not live yet, gaps #2) → POST /{namespace}/issues/{issue}/triage on the Page kindnobody; the Page kind goes back to reviewthe same
To the Marketing Lead to-the-marketing-leada manual action, or a 20 % index drop in 7 daysYour rule (the Site drop Stop)POST /{namespace}/incidents/open → POST /{namespace}/tasks/{task}/assignMarketing Lead, so you, due 1 business day · Take this oneyou
When they write backFixture replyWhat happensWho answersBy whenCall
Interestedthe Page’s form: “Can I see Leaseline for my 30 units in Austin?”instant-response answers it as a new LeadSDR · your agent5 minPOST /{namespace}/conversations/{conversation}/answer
Send me pricingthe form: “What does this cost?”the answer quotes the Pricing Page’s list PriceSDR · your agent5 minPOST /{namespace}/conversations/{conversation}/answer
Not nowthe city update box: an email, no questionthey get the quarterly Austin update, nothing elsenobodyat oncePOST /{namespace}/contacts/{contact}/subscribe
Wrong personthe form: “I’m a tenant, not a manager.”a real next step for a renter, never a sales follow-upSDR · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply to the city updatethe next update waits for the return datenobodythe return datePOST /{namespace}/messages/{message}/schedule
Question it can’t answerthe form: “Where does the 71 % come from?”the method line, then To a person with the questionyou1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“Unsubscribe me from the Austin update.”the update stops for that addressnobodyat oncePOST /{namespace}/contacts/{contact}/unsubscribe

When it hands to you

Growing a batch and every incident come to you: you hold the founder seat, and the Marketing Lead Role has no person. One Brief per Role, as captured (brief is Not live yet, gaps #7).

What lands in each queue · sample
[
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_77",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-10-20T17:00:00Z",
    "draft": "Release batch 4: 120 renewal-rate Pages for the next 120 cities",
    "route": "grow-the-batch",
    "checks": [
      {
        "check": "fact-check",
        "status": "pass"
      },
      {
        "check": "batch-cap",
        "status": "pass"
      }
    ]
  },
  {
    "$id": "https://api.sb/leaseline.app/tasks/task_533",
    "$type": "Task",
    "assignee": "worker_1",
    "due": "2026-10-06T17:00:00Z",
    "on": "site_1",
    "brief": {
      "who": "leaseline.app on Bing",
      "said": "indexed Pages fell 22 % in 7 days",
      "chose": "to-the-marketing-lead",
      "why": "a Site drop of 20 % or more",
      "paused": "every Page kind on Bing",
      "never": [
        "a release while the Incident is open"
      ]
    }
  }
]

You answer with POST /{namespace}/approvals/{approval}/approve or POST /{namespace}/approvals/{approval}/refuse, POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, or POST /{namespace}/actions/{action}/redirect, each a one-call window. An Approval unanswered by its deadline expires, and the batch stays held. A Task nobody claims by its due time goes up a Role through POST /{namespace}/tasks/{task}/escalate, and person-promise fires.

On time: you, 3 of 3 within 1 business day, median 4 h, n 3, ■□□□ Too early to tell beside Other Startups’ 90 % on time. Your queue: in week 1, 2 approvals (the method Page and batch 1) and 1 weekly review; then 1 weekly review and an approval each time a batch grows.

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
fact-check (the record’s Fact gate)a Page whose lead fact is missing, stale, or found on another sourceLead with a fact only your records hold, with its count and date.POST /{namespace}/content/draft
duplicate-page (the record’s gate on matching Pages)a Page that matches another in its cell by wording, meaning or factsMerge it into the Page that owns the intent.POST /{namespace}/content/draft
batch-cap (the record’s Batch cap)a batch over 400 Pages, or one that more than doubles the Site’s indexed PagesSplit the batch, or approve the larger one.POST /{namespace}/approvals/{approval}/approve
claims-resolve (hub)a sentence with no record behind itCut the sentence, or add the record that holds it.POST /{namespace}/content/draft
Stop (pauses the Play)Pauses whenThen
index-stall40 % or more of a batch fetched and left out at the window’s closekill-switch on the batch; it is withdrawn
early-stall25 % or more fetched and left out at day 14the next draw freezes; an Issue opens
impression-floora batch with 0 impressions at the window’s closerelease pauses; its Pages go back to held
site-dropa Site’s indexed Pages fall 20 % or more in 7 daysevery Page kind on that Site pauses; To the Marketing Lead
manual-actionsearch reports a manual action on a Sitethe Page kind is withdrawn there
kill-switchyou press it, or index-stall firesPOST /{namespace}/processes/{process}/pause on content-marketing; released Pages stay up

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

422RefusedThe Eugene Page did not publish: its lead fact is found on another source.
CheckFixThen
fact-check fail · Not live yet, gaps #1Lead with a fact only your records hold, with its count and date.POST /{namespace}/content/draft
duplicate-page pass—POST /{namespace}/content/draft
JSON
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The Eugene Page did not publish: its lead fact is found on another source.",
  "checks": [
    {
      "check": "fact-check",
      "status": "fail",
      "doc": "/plays/programmatic-seo#check-fact-check",
      "fix": "Lead with a fact only your records hold, with its count and date.",
      "then": "Content.draft"
    },
    {
      "check": "duplicate-page",
      "status": "pass"
    }
  ]
}

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

The Stops that fired, as Campaign.paused reasons · sample
[
  {
    "reason": {
      "limit": "site-drop",
      "observed": "22 %",
      "bound": "20 %",
      "window": "7d"
    },
    "doc": "/plays/programmatic-seo#stop-site-drop",
    "task": "task_533"
  }
]

Did it work

Cities were assigned at random: 120 got their Page in a batch, 120 were held back 30 days (rules m–o). Each figure’s call is POST /{namespace}/metrics/query by node and by arm; the leaf and every node above it are Not live yet (gaps #4, #6).

+2.8 visits per indexed Page · ■■■□ Likely · n 240 · path → arr

Chose
grow-the-batch
Why
52 of batch 3’s 60 Pages are indexed at day 28 (87 %), at 3.4 visits each; 120 more stays under the 400-Page cap
From
release-batch · grow-the-batch · hold-for-rework · recheck-fact · withdraw-batch · to-the-marketing-lead
Expects
3.0 or more visits per indexed Page for batch 4 at day 28
Reopens when
batch 4 under 75 % indexed at day 28
By
worker_6
JSON
{
  "$id": "decisions/decision_455",
  "$type": "Decision",
  "choice": "grow-the-batch",
  "reason": "52 of batch 3’s 60 Pages are indexed at day 28 (87 %), at 3.4 visits each; 120 more stays under the 400-Page cap",
  "stakes": "medium",
  "options": [
    "release-batch",
    "grow-the-batch",
    "hold-for-rework",
    "recheck-fact",
    "withdraw-batch",
    "to-the-marketing-lead"
  ],
  "by": "worker_6",
  "on": "collection_7",
  "expected": "3.0 or more visits per indexed Page for batch 4 at day 28",
  "reopensWhen": "batch 4 under 75 % indexed at day 28"
}
NodeAgent halfYour halfAddedConfidenceBasis
indexed-page-yield3.2 visits per indexed Page (307 over 96 Pages)0.4 per city (48 to your existing Pages)+2.8 [1.9, 3.7]■■■□ Likelyheld back at random · n 240 cities · 24 Pages still in their window
icp-reached311 new people reached46+265 [180, 350]■■■□ Likelythe same assignment, first visits only
new-arr ↑$1,041, 3 Customers on Pro · Booked: credited, not all caused by this Play$0, 0 Customers+$1,041 [−$150, +$2,230] · Worth about +$450 a month · Not carrying up yet■□□□ Too early to tell (3 vs 0 Customers)the same · 6 trials still deciding
net-new-arr · arr+$1,041$0+$1,041, carried: it moves no other bridge term■□□□ Too early to tellcarried from new-arr

The leaf counts search and AI-answer visits per Page in the index over the 30 days after release; the held-back cities count visits to your existing Pages for the same queries, to 50 matured Pages in each half. Every city has passed that, so this read stands until batch 4 lands.

How it moves it: each Page leads with a renewal share only Leaseline’s records hold, so 80 % were indexed within 28 days.

Guardrails: treated against held back: signups per visit, 2.9 % against 4.2 % (new-leads per visit), and search visits to the Site’s other Pages, 1,180 against 1,210 a week, so the new Pages took nothing from them.

The Campaign card · sample
{
  "$id": "https://api.sb/leaseline.app/campaigns/programmatic-seo",
  "$type": "Campaign",
  "play": "play:programmatic-seo@1",
  "path": [
    "indexed-page-yield",
    "icp-reached",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "indexed-page-yield",
    "unit": "ratio",
    "heldBack": 120,
    "decision": null
  },
  "readout": {
    "value": 2.8,
    "n": 240,
    "maturing": 24,
    "interval": [
      1.9,
      3.7
    ],
    "readability": "B",
    "basis": "held-back",
    "pooled": false
  },
  "ledger": {
    "spend": 1,
    "workerCost": 5,
    "hours": 1.9,
    "pipelineArr": 0,
    "wonArr": 1041,
    "projectedArr": 455,
    "paybackDays": 5
  }
}

Outside benchmark: the top result gets 1.6 % of clicks on a query that shows an AI overview (a 300,000-Keyword click study, vendor-run).

Before it · After it. Before it: nothing; it starts from your own records. After it: instant-response answers each Page’s form, product-led hears the free signups, and aeo reads the same facts for AI answers.

Example

Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days). 284 city Pages were drafted, 44 held for rework, and 240 cities assigned at random: 120 released in 3 batches of 20, 40 and 60, and 120 held back 30 days. Read every captured Page and thread at ?threads=all; three follow the tables.

What happened, by optionDecisionsPagesResult (counts)
Release the batch3120 renewal-rate Pages96 indexed by day 28 (80 %)
Grow the next batch · Hold for rework · Recheck and redate2 · 44 · 6120 next, approved · 44 held · 6 redated— · 41 back in a batch · 6 kept their rank
Withdraw the batch · To the Marketing Lead1 · 140 turnover Pages · 1 Bing drop18 left out, all 40 withdrawn · back in 4 days
Agent half · your half120 · 120 cities307 visits · 483 Customers ($1,041) · 0
At 30 records (the Use it when volume)about 30 Pagesheld back at random, half delayedOther Startups’ results: +2.1 visits per Page, ■■■□ Likely, n 9,400
DayEventWhat happenedYour time
0event_3002 Campaign.createdthe three steps; the dry run fails approved-pages18 min
0event_3005 Site.publishedthe method Page goes up15 min
1event_3011 Site.publishedbatch 1, 20 Pages, after your approval3 min
6event_3040 Page.indexedthe first Page in the index: Austin—
21 · 56event_3093 Change.merged · event_3188 Readout.createdthe lease count in every title: replayed at 86 % indexed, read back on batch 3 at 87 %, n 605 min
41event_3121 Campaign.pausedsite-drop on Bing, 22 % in 7 days; resumed after 4 days20 min
1–56event_3009 Approval.created, first of 5you approved 3 releases and 2 growths; fact-check refused 3 drafts, each rewritten once; weekly reviews39 min · total 1.9 h measured, 2 h asked

Search, Oct 2 · “lease renewal rate austin” → the Austin Page, first result.

The Page · Of 412 Leaseline-managed leases in Austin that ended in 2026, 292 renewed (71 %).¹ Method: Leaseline lease records, 412 leases, checked Oct 1.² By Leaseline.³

Its form, 14:20 · “Can I see Leaseline for my 30 units?” instant-response answered in 3 min; a trial started at 14:41, and it paid on Oct 16.

Eugene, 2026-09-08 · the Page fact-check refused, then the rewrite that published
- Eugene renters renew about 64 % of the time, per the city’s 2025 housing report.
+ Of 188 Leaseline-managed leases in Eugene that ended in 2026, 121 renewed (64 %), checked Oct 2.

leaseline.app on Bing, 2026-10-03 09:00 · indexed Pages fell 22 % in 7 days · The agent, 09:01 · paused every Page kind on Bing and opened incident_12 (sev3). The Brief went to you (task_533), due 17:00; you claimed it at 11:10 and Bing recrawled by Oct 7.

¹ The lead fact, from Leaseline’s own records, in the first 100 words. ² The method line: the Dataset, the record count and the date checked. ³ The author line names the Startup, never a made-up person.

Your settings

plays/programmatic-seo.mdx
---
holdout: 0.5      # half the cities wait 30 days for their Page
---
One Page per city with 30 or more leases, leading with its 2026 renewal share.
Release batches of 20, then grow each by up to double when 75 % index by day 28.
Put the lease count in every title.
Text
$ npx startup-builder show plays/programmatic-seo
holdout 0.5 · city line 30 leases · batch growth 75 % · count in the title     your file  plays/programmatic-seo.mdx:2,4,5,6 (Ask 2026-08-24; line 6 by change_27, 2026-09-14)
worker role:content-lead, owns never · chooses 6 (4 rule, 2 read) · approval first-send, every-send · checks fact-check, duplicate-page, batch-cap, claims-resolve · stops 6 · placements google-search, bing    catalog  play:programmatic-seo@1 (Not live yet, gaps #5)

Every key from the catalog

KeyIdWhat it is
Tacticpage-kind-buildPage kind build binds a Keyword pattern to one Page kind and one Collection, drafts its Template, and certifies Pages that pass the quality gate. Starts at You approve the first wave.
Tacticbatch-releaseBatch release moves certified Pages into search in growing batches, holds back a random delayed cell, and reads each batch at day 28. Starts at You approve each one.
Tacticindex-stall-stopIndex stall stop trips the kill switch when search declines a batch or Site, withdraws its Pages, and returns the Page kind to review. Starts at Sends nothing.
Tacticfact-recheck-pruneFact recheck and prune re-runs each lead fact on its cadence, moves a Page date only on a real change, and withdraws Pages search ignores. Starts at Sends nothing.
Triggerkeyword-pattern-demandKeyword pattern demand: threshold on Keyword.measured, window 30d.
Triggerrecord-set-readyRecord set ready: threshold on Collection.filled, window 30d.
Triggerbatch-window-closedBatch window closed: at on Site.published, window 7d.
Triggercrawled-not-indexedCrawled not indexed: state on Page.unindexed, window 14d.
Triggersite-index-dropSite index drop: threshold on Metric.watched, window 3d.
Triggerlead-fact-changedLead fact changed: event on Fact.changed, window 7d.
Triggerpage-search-silencePage search silence: threshold on Metric.watched, window 30d.
Benchmarkpage-count-ai-visibilityRank correlation between a brand's page count and its AI overview visibility.: 0.17 ratio (75,000-brand AI overview correlation study (vendor-run)., 2025-05-26).
Benchmarkposition-one-ctr-ai-overviewTop-result click-through rate on a Keyword that shows an AI overview.: 1.6 % (300,000-Keyword click study (vendor-run)., 2026-02-04).
Benchmarkresult-click-share-ai-summaryShare of searches with a click on a result when an AI summary shows.: 8 % (Pew Research Center, 2025-07-22).

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