URL: /plays/programmatic-seo · Id: `play:programmatic-seo`

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

Turns each record only you hold into one search [Page](https://startupbuilder.dev/business/nouns/page) that leads with its fact, released in batches that stop when search ignores them.

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

```text title="Run one · Ask"
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.
```

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

```json title="Run one · Response · sample"
{
  "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"
    ],
    "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"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_6",
      "verb": "Metric.query",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_6",
      "verb": "Keyword.measure",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_3",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingApproval",
      "mode": "try",
      "by": "worker_6",
      "verb": "Site.publish",
      "checks": [
        {
          "check": "fact-check",
          "status": "pass"
        },
        {
          "check": "duplicate-page",
          "status": "pass"
        },
        {
          "check": "batch-cap",
          "status": "pass"
        },
        {
          "check": "claims-resolve",
          "status": "pass"
        }
      ]
    }
  ]
}
```

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

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$450 ARR a month at 130 Pages | 40 min week 1, then 10 a week | $0.02 per Page, ~$2.60 a month at 130 | Day 6: the first Page indexed | 30+ records with a fact only you hold | Under 30: `editorial-seo` · no Site of your own: publish one (<Op verb="Site.publish" />) |

**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
```bash
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"
```


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

| Check | Fix (one call) | Your time | Wait |
| --- | --- | --- | --- |
| <a id="check-use-when"></a>`use-when` · <a id="check-baseline"></a>`baseline` · pass: 62 cities, demand in 58; 0.4 visits per city a month, n 120 (gaps #8) | <Op verb="Keyword.measure" /> on the pattern, then <Op verb="Metric.query" /> on `indexed-page-yield`, which fills Before | 2 min | none |
| <a id="check-approved-pages"></a>`approved-pages` · fail: no method Page | <Op verb="Site.publish" /> the method Page | 15 min | none |
| <a id="check-fact-check"></a>`fact-check` · warn: 19 cities under 30 leases | <Op verb="Content.draft" /> holds them; they join a batch at 30 | 0 min | until each city reaches 30 leases |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn (gaps #8): no Marketing Lead | <Op verb="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 <Op verb="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 <Op verb="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 <Op verb="Site.publish" /> |
| Search fetched a Page and left it out | `crawled-not-indexed` | `state` on `Page.unindexed` where status = 'unindexed' after a fetch | 14d | <Op verb="Integration.connect" /> with your search console |
| + 4 more for query demand, index drops, changed facts and silent Pages (`?lanes=all`) | | | | |

```json title="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"
  }
}
```

```bash title="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.

| 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 | <Op verb="Action.cancel" /> before the release |
| Hold for rework · Withdraw the batch | It does it | 44 of 44 · 1 of 1 | — | — | <Op verb="Action.redirect" /> |
| Grow the next batch | You approve each one | 2 of 2 | never: its stakes are medium | — | <Op verb="Approval.approve" /> · <Op verb="Approval.refuse" /> |
| To the Marketing Lead | It does it | 1 of 1 | — | — | <Op verb="Task.claim" /> |
| Change the Page kind’s Template | You approve each one | 1 merged of 1 | never | — | <Op verb="Change.merge" /> · <Op verb="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 <Op verb="Site.publish" />, <Op verb="Content.draft" />, `Content.withdraw` (Not live yet, gaps #2), <Op verb="Issue.triage" />, <Op verb="Incident.open" />, <Op verb="Task.assign" />, and any other call becomes an Approval under the Role’s Mandate.

```md title="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 <Op verb="Change.merge" />. **Replayed on past batches**, <Op verb="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; <Op verb="Role.staff" /> seats it.

```bash
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` → <Op verb="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 | <Op verb="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 | <Op verb="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) | <Op verb="Content.draft" /> → <Op verb="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) → <Op verb="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) | <Op verb="Incident.open" /> → <Op verb="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 | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Not now | the city update box: an email, no question | they get the quarterly Austin update, nothing else | nobody | at once | <Op verb="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 | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply to the city update | the next update waits for the return date | nobody | the return date | <Op verb="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 | <Op verb="Conversation.escalate" /> |
| Stop | “Unsubscribe me from the Austin update.” | the update stops for that address | nobody | at once | <Op verb="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).

```json title="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 <Op verb="Approval.approve" /> or <Op verb="Approval.refuse" />, <Op verb="Task.claim" /> (Take this one), <Op verb="Task.return" /> with a reason, or <Op verb="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 <Op verb="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:** <Op verb="Deal.assign" /> sets `owner` to the receiving person, the agent is never a Deal’s owner, and Meetings land on the owner’s calendar under the owner’s name.

## When it won’t send
| Check (refuses one send) | Refuses | Fix | Then |
| --- | --- | --- | --- |
| `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. | <Op verb="Content.draft" /> |
| <a id="check-duplicate-page"></a>`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. | <Op verb="Content.draft" /> |
| <a id="check-batch-cap"></a>`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. | <Op verb="Approval.approve" /> |
| <a id="check-claims-resolve"></a>`claims-resolve` (hub) | a sentence with no record behind it | Cut the sentence, or add the record that holds it. | <Op verb="Content.draft" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-index-stall"></a>`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 |
| <a id="stop-early-stall"></a>`early-stall` | 25 % or more fetched and left out at day 14 | the next draw freezes; an Issue opens |
| <a id="stop-impression-floor"></a>`impression-floor` | a batch with 0 impressions at the window’s close | release pauses; its Pages go back to held |
| <a id="stop-site-drop"></a>`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 |
| <a id="stop-manual-action"></a>`manual-action` | search reports a manual action on a Site | the Page kind is withdrawn there |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it, or `index-stall` fires | <Op verb="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.

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


```json title="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 <Op verb="Metric.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`

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

```json title="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 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-response` answered in 3 min; a trial started at 14:41, and it paid on Oct 16.

```diff title="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
```mdx title="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
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-page-kind-build"></a>`page-kind-build` | Page 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. |
| Tactic | <a id="tactic-batch-release"></a>`batch-release` | Batch 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. |
| Tactic | <a id="tactic-index-stall-stop"></a>`index-stall-stop` | Index 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. |
| Tactic | <a id="tactic-fact-recheck-prune"></a>`fact-recheck-prune` | Fact 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. |
| Trigger | <a id="trigger-keyword-pattern-demand"></a>`keyword-pattern-demand` | Keyword pattern demand: `threshold` on `Keyword.measured`, window 30d. |
| Trigger | <a id="trigger-record-set-ready"></a>`record-set-ready` | Record set ready: `threshold` on `Collection.filled`, window 30d. |
| Trigger | <a id="trigger-batch-window-closed"></a>`batch-window-closed` | Batch window closed: `at` on `Site.published`, window 7d. |
| Trigger | <a id="trigger-crawled-not-indexed"></a>`crawled-not-indexed` | Crawled not indexed: `state` on `Page.unindexed`, window 14d. |
| Trigger | <a id="trigger-site-index-drop"></a>`site-index-drop` | Site index drop: `threshold` on `Metric.watched`, window 3d. |
| Trigger | <a id="trigger-lead-fact-changed"></a>`lead-fact-changed` | Lead fact changed: `event` on `Fact.changed`, window 7d. |
| Trigger | <a id="trigger-page-search-silence"></a>`page-search-silence` | Page search silence: `threshold` on `Metric.watched`, window 30d. |
| Benchmark | <a id="benchmark-page-count-ai-visibility"></a>`page-count-ai-visibility` | Rank 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). |
| Benchmark | <a id="benchmark-position-one-ctr-ai-overview"></a>`position-one-ctr-ai-overview` | Top-result click-through rate on a Keyword that shows an AI overview.: 1.6 % (300,000-Keyword click study (vendor-run)., 2026-02-04). |
| Benchmark | <a id="benchmark-result-click-share-ai-summary"></a>`result-click-share-ai-summary` | Share of searches with a click on a result when an AI summary shows.: 8 % (Pew Research Center, 2025-07-22). |