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.
- Step
- Get found
- Channel
- Blog and search
- Kind
- Search
- Summary
- startupbuilder.dev
Be our Content Lead: make one search Page per city where we manage 30 or more leases, leading with that city’s renewal share from our own records. Release them in small batches and pull any batch search ignores. Hold half the cities back 30 days. Show me what’s missing; don’t publish.
# Not live yet (gaps #2)
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"}'import { $ } from 'startup-builder'
return await $.Campaign.create({ play: 'programmatic-seo' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2
npx startup-builder do Campaign.create --json '{"play":"programmatic-seo"}' --try # Not live yet (gaps #2){
"name": "do",
"arguments": {
"code": "return await $.Campaign.create({ play: 'programmatic-seo' }, { mode: 'try' }) // dry run on the day-0 Startup, 2026-08-24 · Not live yet, gaps #2",
"mode": "try"
}
}application/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"
},
…
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"
}
}application/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 yetPreviewOn 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 batch | Choice | Reason |
|---|---|---|
| Batch 3, 60 renewal-rate Pages: 52 indexed at day 28 | grow-the-batch | 87 % indexed, 3.4 visits each |
| Dallas Page: its 2026 renewal share moved from 68 % to 71 % | recheck-fact | the lead fact changed; the date moves with it |
| Turnover Pages, batch 1: 18 of 40 left out of the index at day 28 | withdraw-batch | 45 % left out passes the 40 % stop |
| Eugene Page draft: its lead fact matches a public housing report | hold-for-rework | a fact another source states is no lead |
- 40 min week 1, then 10 a week
- Your time
- Day 6: the first Page indexed
- First signal
- 30+ records with a fact only you hold
- Use it when
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/
Start it this week
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"| Check | Fix | Then |
|---|---|---|
use-when pass · Not live yet, gaps #8 | Keep 30 or more records with a fact of their own, or run editorial-seo. | POST /{namespace}/metrics/query |
baseline pass · Not live yet, gaps #8 | Read it once; it fills Before in Your number. | POST /{namespace}/metrics/query |
approved-pages fail · Not live yet, gaps #8 | Publish the method Page every city Page links to. | POST /{namespace}/sites/{site}/publish |
fact-check warn · Not live yet, gaps #1 | Those 19 stay held until each reaches 30 leases. | POST /{namespace}/content/draft |
role-unstaffed warn · Not live yet, gaps #8 | Staff 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.
| Check | Fix (one call) | Your time | Wait |
|---|---|---|---|
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 Before | 2 min | none |
approved-pages · fail: no method Page | POST /{namespace}/sites/{site}/publish the method Page | 15 min | none |
fact-check · warn: 19 cities under 30 leases | POST /{namespace}/content/draft holds them; they join a batch at 30 | 0 min | until each city reaches 30 leases |
role-unstaffed · warn (gaps #8): no Marketing Lead | POST /{namespace}/roles/{role}/staff, or leave it: incidents and approvals come to you | 0 min | none |
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
| When | Trigger id | Starts on | Window | How it gets in |
|---|---|---|---|---|
| A city reaches 30 complete lease records | record-set-ready | threshold on Collection.filled where complete records >= the Page kind's minimum | into the next batch within 30d | your records sync through POST /{namespace}/integrations/{integration}/sync |
| A released batch reaches day 28 | batch-window-closed | at on Site.published where 28 days after the batch's Site.published | decide within 7d | the clock, from the batch’s POST /{namespace}/sites/{site}/publish |
| Search fetched a Page and left it out | crawled-not-indexed | state on Page.unindexed where status = 'unindexed' after a fetch | 14d | POST /{namespace}/integrations/connect with your search console |
+ 4 more for query demand, index drops, changed facts and silent Pages (?lanes=) |
{
"$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"
}
}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:, 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.
| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
|---|---|---|---|---|---|
| Release the batch · Recheck and redate | It does it, you can stop it | 3 of 3 · 6 of 6 | met after batch 2 (both read clean at day 28) | any Page fails fact-check after release → You approve each one | POST /{namespace}/actions/{action}/cancel before the release |
| Hold for rework · Withdraw the batch | It does it | 44 of 44 · 1 of 1 | — | — | POST /{namespace}/actions/{action}/redirect |
| Grow the next batch | You approve each one | 2 of 2 | never: its stakes are medium | — | POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/approvals/{approval}/refuse |
| To the Marketing Lead | It does it | 1 of 1 | — | — | POST /{namespace}/tasks/{task}/claim |
| Change the Page kind’s Template | You approve each one | 1 merged of 1 | never | — | POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert |
| Never: a rating no Customer gave, a fact from another source, a made-up author | Refused, every time | 3 drafts refused | never | — | 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.
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.
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
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
|---|---|---|---|---|---|
Release the batch release-batch | certified Pages pass the fact gate, under the cap | Your rule (line 5) | publish-content → POST /{namespace}/sites/{site}/publish | nobody | the same |
Grow the next batch grow-the-batch | the last batch indexed 75 % or more by day 28 | Agent’s call, with its reason | POST /{namespace}/sites/{site}/publish on the next, larger batch | you approve it, due 1 business day · Take this one | you |
Hold for rework hold-for-rework | a lead fact missing or stale, or a Page that reads like another | Agent’s call, with its reason | POST /{namespace}/content/draft back to held | nobody | the same |
Recheck and redate recheck-fact | the fact a Page leads with changed | Your rule (copy rule 5) | POST /{namespace}/content/draft → POST /{namespace}/sites/{site}/publish, with the new date | nobody | the same |
Withdraw the batch withdraw-batch | search leaves 40 % or more out at the window’s close | Your rule (the index stall Stop) | Content.withdraw (Not live yet, gaps #2) → POST /{namespace}/issues/{issue}/triage on the Page kind | nobody; the Page kind goes back to review | the same |
To the Marketing Lead to-the-marketing-lead | a manual action, or a 20 % index drop in 7 days | Your rule (the Site drop Stop) | POST /{namespace}/incidents/open → POST /{namespace}/tasks/{task}/assign | Marketing Lead, so you, due 1 business day · Take this one | you |
| When they write back | Fixture reply | What happens | Who answers | By when | Call |
|---|---|---|---|---|---|
| Interested | the Page’s form: “Can I see Leaseline for my 30 units in Austin?” | instant-response answers it as a new Lead | SDR · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Send me pricing | the form: “What does this cost?” | the answer quotes the Pricing Page’s list Price | SDR · your agent | 5 min | POST /{namespace}/conversations/{conversation}/answer |
| Not now | the city update box: an email, no question | they get the quarterly Austin update, nothing else | nobody | at once | POST /{namespace}/contacts/{contact}/subscribe |
| Wrong person | the form: “I’m a tenant, not a manager.” | a real next step for a renter, never a sales follow-up | SDR · your agent | 1 business day | POST /{namespace}/conversations/{conversation}/answer |
| Out of office | an auto-reply to the city update | the next update waits for the return date | nobody | the return date | POST /{namespace}/messages/{message}/schedule |
| Question it can’t answer | the form: “Where does the 71 % come from?” | the method line, then To a person with the question | you | 1 business day | POST /{namespace}/conversations/{conversation}/escalate |
| Stop | “Unsubscribe me from the Austin update.” | the update stops for that address | nobody | at once | POST /{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).
[
{
"$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) | Refuses | Fix | Then |
|---|---|---|---|
fact-check (the record’s Fact gate) | a Page whose lead fact is missing, stale, or found on another source | Lead 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 facts | Merge 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 Pages | Split the batch, or approve the larger one. | POST /{namespace}/approvals/{approval}/approve |
claims-resolve (hub) | a sentence with no record behind it | Cut the sentence, or add the record that holds it. | POST /{namespace}/content/draft |
| Stop (pauses the Play) | Pauses when | Then |
|---|---|---|
index-stall | 40 % or more of a batch fetched and left out at the window’s close | kill-switch on the batch; it is withdrawn |
early-stall | 25 % or more fetched and left out at day 14 | the next draw freezes; an Issue opens |
impression-floor | a batch with 0 impressions at the window’s close | release pauses; its Pages go back to held |
site-drop | a Site’s indexed Pages fall 20 % or more in 7 days | every Page kind on that Site pauses; To the Marketing Lead |
manual-action | search reports a manual action on a Site | the Page kind is withdrawn there |
kill-switch | you press it, or index-stall fires | POST /{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.
| Check | Fix | Then |
|---|---|---|
fact-check fail · Not live yet, gaps #1 | Lead 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.
[
{
"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"
}| Node | Agent half | Your half | Added | Confidence | Basis |
|---|---|---|---|---|---|
indexed-page-yield | 3.2 visits per indexed Page (307 over 96 Pages) | 0.4 per city (48 to your existing Pages) | +2.8 [1.9, 3.7] | ■■■□ Likely | held back at random · n 240 cities · 24 Pages still in their window |
icp-reached | 311 new people reached | 46 | +265 [180, 350] | ■■■□ Likely | the 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 tell | carried 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.
{
"$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=; three follow the tables.
| What happened, by option | Decisions | Pages | Result (counts) |
|---|---|---|---|
| Release the batch | 3 | 120 renewal-rate Pages | 96 indexed by day 28 (80 %) |
| Grow the next batch · Hold for rework · Recheck and redate | 2 · 44 · 6 | 120 next, approved · 44 held · 6 redated | — · 41 back in a batch · 6 kept their rank |
| Withdraw the batch · To the Marketing Lead | 1 · 1 | 40 turnover Pages · 1 Bing drop | 18 left out, all 40 withdrawn · back in 4 days |
| Agent half · your half | 120 · 120 cities | 307 visits · 48 | 3 Customers ($1,041) · 0 |
| At 30 records (the Use it when volume) | about 30 Pages | held back at random, half delayed | Other Startups’ results: +2.1 visits per Page, ■■■□ Likely, n 9,400 |
| Day | Event | What happened | Your time |
|---|---|---|---|
| 0 | event_3002 Campaign.created | the three steps; the dry run fails approved-pages | 18 min |
| 0 | event_3005 Site.published | the method Page goes up | 15 min |
| 1 | event_3011 Site.published | batch 1, 20 Pages, after your approval | 3 min |
| 6 | event_3040 Page.indexed | the first Page in the index: Austin | — |
| 21 · 56 | event_3093 Change.merged · event_3188 Readout.created | the lease count in every title: replayed at 86 % indexed, read back on batch 3 at 87 %, n 60 | 5 min |
| 41 | event_3121 Campaign.paused | site-drop on Bing, 22 % in 7 days; resumed after 4 days | 20 min |
| 1–56 | event_3009 Approval.created, first of 5 | you approved 3 releases and 2 growths; fact-check refused 3 drafts, each rewritten once; weekly reviews | 39 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-responseanswered in 3 min; a trial started at 14:41, and it paid on Oct 16.
- 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
---
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.$ 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
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.