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

# Editorial SEO

> Editorial SEO writes the guides and honest comparisons your buyers search for, from your own records, then refreshes or prunes each Page that slips.

Editorial SEO writes the guides and honest comparisons your buyers search for, from your own records, then refreshes or prunes each [Page](https://startupbuilder.dev/business/nouns/page) that slips.

```text title="Set it up · Ask"
Be our Content Lead: write the guides and honest comparisons small property managers search for, from our own records. Hold back a third of the topics so we can tell. I approve every rival row. Show me what’s missing; don’t send.
```

```text title="Run one · Ask"
Hand our latest search finding to the Content Lead agent and show me what it chose and why. Don’t send.
```

```ts title="Run one · SDK"
import { $ } from 'startup-builder'

const { events: [last] } = await $.Event.list({ where: 'name in ("Keyword.measured", "Monitor.checked", "Fact.changed", "Page.declined", "Monitor.matched", "Form.submitted")', 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_462",
    "$type": "Decision",
    "choice": "write-a-comparison",
    "reason": "alternatives to the largest property-management suite, for under 100 units, runs 2,400 searches a month, over your 300 floor, and no Page of yours targets it",
    "stakes": "medium",
    "options": [
      "write-a-comparison",
      "write-a-guide",
      "refresh-the-page",
      "merge-or-prune",
      "ask-for-a-mention",
      "hide-a-disputed-row"
    ],
    "by": "worker_7",
    "on": "keyword_44",
    "expected": "the Page indexed within 28d, and organic visits past 100 a month by day 60",
    "reopensWhen": "Monitor.matched page-not-indexed for keyword_44’s Page within 28d"
  },
  "actions": [
    {
      "$id": "https://api.sb/leaseline.app/actions/action_1",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_7",
      "verb": "Keyword.measure",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_2",
      "$type": "Action",
      "status": "done",
      "mode": "try",
      "by": "worker_7",
      "verb": "Content.draft",
      "checks": [
        {
          "check": "claims-resolve",
          "status": "pass"
        },
        {
          "check": "fact-check",
          "status": "pass"
        }
      ]
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_3",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingApproval",
      "mode": "try",
      "by": "worker_7",
      "verb": "Approval.create",
      "checks": []
    },
    {
      "$id": "https://api.sb/leaseline.app/actions/action_4",
      "$type": "Action",
      "status": "pending",
      "reason": "awaitingApproval",
      "mode": "try",
      "by": "worker_7",
      "verb": "Content.publish",
      "checks": [
        {
          "check": "claims-resolve",
          "status": "pass"
        },
        {
          "check": "fact-check",
          "status": "pass"
        },
        {
          "check": "dispute-hold",
          "status": "pass"
        }
      ]
    }
  ]
}
```

`action_3` sends you the 14 rival rows, each with its capture; `action_4` publishes once you approve them. The Page names Leaseline as its publisher and seller in its first 60 words.

`+118 visits per family a month · ■■□□ Early sign · n 24 families · path → arr`

| Try another: the query family | Choice | Reason |
| --- | --- | --- |
| Alternatives to the largest suite, for under 100 units · 2,400 a month · no Page | `write-a-comparison` | over the 300 floor; 14 rival rows go to you |
| How to send a 60-day renewal notice · 880 a month | `write-a-guide` | your records answer it: 3,520 renewals |
| Guide “Lease renewal letter template” dropped 4 positions on two weekly reads | `refresh-the-page` | its example still shows a 30-day notice; the template changed Oct 2 |
| Guide on move-out checklists: never indexed after 90 days | `merge-or-prune` | the move-out guide answers it; merged behind a redirect |
| A rival writes: “Your row says we charge per unit. We don’t.” | `hide-a-disputed-row` | a written dispute hides the row within 24 h until you review it |

| Worth | Your time | Spend | First signal | Use it when | Instead |
| --- | --- | --- | --- | --- | --- |
| ≈ +$490 ARR a month at 4 Pages | 70 min week 1, then 15 a day | $0.25 per market-data read, ~$1 a month | Day 12: first Page indexed | A published Price, 3+ searched families with no Page | No published Price: publish it (<Op verb="Site.publish" />) · no family clears the floor: `aeo` |

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

Worth’s formula: about +118 organic visits per Page a month × 4 new Pages a month × 0.6 ICP share × 0.5 paid per 100 visits × $347 first order ARR ≈ +$491. Under First signal: About 12 weeks to tell the leaf’s +118 lift per family at 16 and 8 families; dollars take longer, so `new-arr` reads Too early to tell.

`sample · api.sb/leaseline.app · 41 Customers at 2026-10-19 · on Keyword.measured · first draft 22m · 8 keys Not live yet`

## Start it this week
```bash
npx startup-builder add play editorial-seo   # 1 · writes plays/editorial-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":"editorial-seo"}'

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

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

# off: no new Page or request; published Pages stay up
curl -X PATCH https://api.sb/leaseline.app/campaigns/editorial-seo \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endsAt":"now"}'
```


```json title="The dry run’s 422 · 2026-08-24 · sample"
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "editorial-seo can’t start yet: 1 check fails and 1 warns.",
  "checks": [
    {
      "check": "use-when",
      "status": "pass",
      "observed": "22 searched query families with no Page of yours; Pricing Page live; 41 Customers",
      "doc": "/plays/editorial-seo#check-use-when",
      "fix": "None needed: 3 families are enough.",
      "then": "Metric.query"
    },
    {
      "check": "baseline",
      "status": "pass",
      "observed": "1,240 organic visits in 90 days, 6 Pages",
      "doc": "/plays/editorial-seo#check-baseline",
      "fix": "None needed: 1,240 in 90 days is your Before.",
      "then": "Metric.query"
    },
    {
      "check": "approved-pages",
      "status": "fail",
      "doc": "/plays/editorial-seo#check-approved-pages",
      "fix": "Publish dated fact records for Price, plans and 3 integrations before a row cites them.",
      "then": "Site.publish"
    },
    {
      "check": "role-unstaffed",
      "status": "warn",
      "roles": [
        "content-lead"
      ],
      "owned": 0,
      "doc": "/plays/editorial-seo#check-role-unstaffed",
      "fix": "Staff the Content Lead, or leave it: every rival row comes 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: 22 families with no Page; 1,240 organic visits in 90 days (gaps #8) | <Op verb="Metric.query" /> on `organic-visits`, which fills Before in Your number | 1 min | none |
| <a id="check-approved-pages"></a>`approved-pages` · fail (Not live yet) | <Op verb="Site.publish" /> the facts Page with Price, plans and 3 integrations | 25 min | none |
| <a id="check-role-unstaffed"></a>`role-unstaffed` · warn (gaps #8): no Content Lead seat | <Op verb="Role.staff" />, or leave it: every rival row comes to you | 0 min | none |

`Your number · Before 22 per family · Now 140 organic visits per family a month · Target 150 by Dec 31 · ■■□□ Early sign · held back at random · n 24 families`

Each market-data read costs about $0.25 (Worker cost), about 4 a month, and drafting a Page costs about $0.30. That comes to about $1 a month at 4 Pages, beside your 10 minutes per comparison.

## When it starts
| When | Trigger id | Starts on | Window | How it gets in |
| --- | --- | --- | --- | --- |
| A searched query family clears the floor with no Page of yours | `comparison-query-found` | `threshold` on `Keyword.measured` where family.volume >= demand floor and no Page targets it | read weekly; 90d | <Op verb="Keyword.create" /> for each family; <Op verb="Keyword.measure" /> reads it weekly |
| A Page drops 3+ positions on two weekly reads | `page-rank-loss` | `threshold` on `Keyword.measured` where position drop >= 3 on 2 weekly reads in a row | 14d | the same weekly <Op verb="Keyword.measure" /> |
| + 3 more for changed facts, slipping Pages and Pages never indexed (`?lanes=all`) | | | | |

```json title="One signed delivery · StartupBuilder-Signature"
{
  "$id": "https://api.sb/leaseline.app/events/event_1512",
  "$type": "Event",
  "name": "Keyword.measured",
  "at": "2026-10-19T05:30:44Z",
  "seq": 1512,
  "data": {
    "keyword": {
      "$id": "https://api.sb/leaseline.app/keywords/keyword_44",
      "family": "alternatives to the largest property-management suite, under 100 units",
      "volume": 2400,
      "floor": 300,
      "page": null
    }
  }
}
```

```bash title="Reproduce it in test"
npx startup-builder trigger Keyword.measured --like event_1512   # replays the delivery
npx startup-builder clock advance --by 28d   # then runs the index check on the new Page
npx startup-builder listen Keyword.measured --forward-to http://localhost:3000/hook
```

## Who runs it
**Content Lead · your agent** (`role:content-lead`, `worker_7`) holds the Key Result. It drafts every Page from your records and captures every rival row; each comparison and each disputed row comes to **you** (Dana Okafor, person, `worker_1`), who approves the rows and the byline. Your number links to Did it work.

| Option | Rung today | Evidence (passed of n) | Moves up at | Drops back when | You can |
| --- | --- | --- | --- | --- | --- |
| Write a guide · Refresh the Page | It does it, you can stop it | 9 of 9 · 4 of 4 | met on day 19 (the first 5 guides) | a published sentence fails `claims-resolve` → You approve each one | <Op verb="Action.cancel" /> inside the hold |
| Write a comparison · Hide a disputed row | You approve each one | 7 of 7, 92 rows · 1 of 1 | never: rival rows stay with you | — | <Op verb="Approval.approve" /> · <Op verb="Task.claim" /> |
| Merge or prune · Ask for a mention | It does it | 2 of 2 · 6 of 6 | — | a mention complaint → paused | <Op verb="Action.redirect" /> |
| Change the backlog rule | You approve each one | 1 merged of 1 | never | — | <Op verb="Change.merge" /> · <Op verb="Change.revert" /> |
| Never: a ranked list with you in it, an estimated rival price, a date moved with no fact changed, an invented byline | To a person, every time | 0 asked | never | — | <Op verb="Task.claim" /> |

**What it reads · What it may call.** It reads your fact records, rival Pages as they publish them, and weekly search reads. A Visit, a rank or a rival change picks which Page to work and never appears in a Message. Its tools are <Op verb="Content.draft" />, <Op verb="Approval.create" />, <Op verb="Content.publish" />, <Op verb="Content.update" />, <Op verb="Page.update" />, <Op verb="Message.schedule" />, <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:editorial-seo@1 · each line names its record key"
You hold the Content Lead Key Result: 150 organic visits per family a month by 2026-12-31. ← worker, metric
On each weekly read or change, choose one option and say why.                    ← triggers, chooses
Name Leaseline as publisher and seller in the first 60 words.                    ← copyRules 2
Show every tested row a rival wins; never estimate a hidden price.               ← copyRules 6, 7
Move a Page’s date only when a Fact it states changes.                           ← copyRules 4
Hold back 1 family in 3 at random; never draft a held-back family.               ← your file, line 2
```

**The slow loop, as a Change.** `decision_421` (2026-09-21, weekly) read 3 kept families under 400 searches that drew under 20 visits a month each, so `change_27` raised the demand floor from 200 to 300 (your file, line 4) and Dana merged it with <Op verb="Change.merge" />. **Replayed on past reads**, <Op verb="Change.review" /> dropped 3 of 22 families; read back at +28d, the kept families average 140 visits, n 16, ■■□□ Early sign.

**Bring your own agent** (folded) starts at It does it, you can stop it on guides when it matches or beats the seated agent on the eval suite; comparisons stay with you either way; <Op verb="Role.staff" /> seats it.

```bash
npx startup-builder eval --scenario editorial-seo-24 --agent "claude -p" --seat content-lead   # seated: 92 of 92 rows captured, 0 Checks failed sent, 14 of 16 indexed · yours: 90 of 92, 2 failed `fact-check`
```

## What it can do next
| Option | Picked when | Decided by | Runs | Hands to | If it’s just you |
| --- | --- | --- | --- | --- | --- |
| Write a comparison `write-a-comparison` | a versus, alternatives or rival-pair family clears the floor | Your rule (line 4) | `publish-content` → <Op verb="Content.draft" /> with captures → <Op verb="Approval.create" /> for the rows | <Op verb="Task.assign" /> → you, due 1 business day · Take this one | you |
| Write a guide `write-a-guide` | a how-to family clears the floor, and your records answer it | Agent’s call, with its reason | `publish-content` → <Op verb="Content.draft" /> → <Op verb="Content.publish" /> | nobody | the same |
| Refresh the Page `refresh-the-page` | a cited fact or rival Page changed, or a Page slipped | Agent’s call, with its reason | <Op verb="Content.update" /> → <Op verb="Content.publish" /> | nobody | the same |
| Merge or prune `merge-or-prune` | never indexed after 90 days, or another Page answers it | Agent’s call, with its reason | <Op verb="Page.update" /> to the nearest Page, behind a redirect | nobody | the same |
| Ask for a mention `ask-for-a-mention` | a ranking Page misstates you or leaves you out | Agent’s call, with its reason | `editorial-mention-request` → <Op verb="Message.schedule" /> | the Page’s author | the same |
| Hide a disputed row `hide-a-disputed-row` | a rival disputes a row in writing | Your rule (the dispute hold) | <Op verb="Page.update" /> hides the row → <Op verb="Task.assign" /> | you review it, due 1 business day · Take this one | you |

| When they write back | Fixture reply | What happens | Who answers | By when | Call |
| --- | --- | --- | --- | --- | --- |
| Interested | “Happy to add you. Send a line and a link.” | answers with one sourced line and the Page link, never a link ask of its own | Content Lead · your agent | 1 business day | <Op verb="Conversation.answer" /> |
| Send me pricing | “What does Leaseline cost at 40 units?” | answers with the Pricing Page’s list Price, $29 a month | Content Lead · your agent | 1 business day | <Op verb="Conversation.answer" /> |
| Not now | “We revise that list in January.” | the follow-up stops; the Decision reopens at their revision | nobody | at once | <Op verb="Enrollment.cancel" /> |
| Wrong person | “Our editor handles that list.” | asks them to forward it; it never writes to an address from their text | Content Lead · your agent | 1 business day | <Op verb="Conversation.answer" /> |
| Out of office | an auto-reply until Oct 28 | the follow-up waits for the return date | nobody | the return date | <Op verb="Message.schedule" /> |
| Question it can’t answer | “Would you pay for a placement?” | To a person: paid placement is your call, and it carries their sponsored label | you | 1 business day | <Op verb="Conversation.escalate" /> |
| Stop | “Please don’t email us again.” | that author is suppressed for all your Startups | nobody | at once | <Op verb="Contact.suppress" /> |

## When it hands to you
Comparisons and disputed rows come to **you**: every rival row needs your OK. 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/tasks/task_588",
    "$type": "Task",
    "assignee": "worker_1",
    "due": "2026-10-20T05:30:00Z",
    "on": "keyword_44",
    "brief": {
      "what": "Alternatives to the largest property-management suite, for under 100 units",
      "rows": 14,
      "captured": "2026-10-18, from their own public Pages",
      "rivalWins": [
        "owner portals",
        "accounting"
      ],
      "blank": [
        "their price under 100 units: not published"
      ],
      "never": [
        "an estimated price",
        "a ranked list with Leaseline in it"
      ]
    }
  },
  {
    "$id": "https://api.sb/leaseline.app/approvals/approval_71",
    "$type": "Approval",
    "verdict": null,
    "deadline": "2026-10-20T05:30:00Z",
    "draft": "Published by Leaseline, which sells renewal software. For under 100 units, three options fit…",
    "route": "write-a-comparison",
    "checks": [
      {
        "check": "claims-resolve",
        "status": "pass"
      },
      {
        "check": "fact-check",
        "status": "pass"
      }
    ]
  }
]
```

You answer with <Op verb="Task.claim" /> (Take this one), <Op verb="Task.return" /> with a reason, <Op verb="Approval.approve" /> or <Op verb="Approval.refuse" />, each a one-call window; an Approval unanswered by its deadline expires and the Page stays in draft. Once you claim a dispute, the agent’s pending edits on that Page cancel with `claimedByPerson` (Not live yet, gaps #7). A Task nobody claims by its due time goes up through <Op verb="Task.escalate" />, and `person-promise` fires.

**On time:** you, 8 of 8 claimed within their due time, median 4 h 20 min, n 8, ■□□□ Too early to tell beside Other Startups’ 93 % on time. **Your queue:** counted by kind: in week 1, 3 comparisons and 2 guides; since then about 1 comparison a week, 1 dispute in the window and the weekly review.

**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 |
| --- | --- | --- | --- |
| <a id="check-owned-by-a-person"></a>`owned-by-a-person` (rule j; Not live yet, gaps #8) | a mention request to an author whose Conversation a person claimed | Leave it to the person who owns it: the draft waits in their queue. | <Op verb="Task.assign" /> |
| <a id="check-claims-resolve"></a>`claims-resolve` (shared) | a sentence the claims source or a rival capture lacks | Cut the claim, or name the record that backs it. | <Op verb="Content.draft" /> |
| <a id="check-fact-check"></a>`fact-check` (shared) | a rival row with no capture, or a price the rival does not publish | Capture the rival’s own Page, or leave the row blank. | <Op verb="Content.draft" /> |
| <a id="check-dispute-hold"></a>`dispute-hold` (the record’s Dispute hold) | a row a rival disputed in writing, until you review it | Review it with a fresh capture; it stays hidden meanwhile. | <Op verb="Task.claim" /> |

| Stop (pauses the Play) | Pauses when | Then |
| --- | --- | --- |
| <a id="stop-visibility"></a>`visibility` | site-wide organic visits 25 % under the 28-day baseline within 14 days of a search update | new publishes pause; You approve each one |
| <a id="stop-index"></a>`index` | over 30 % of the Play’s Pages crawled, not indexed, at 28 days | new publishes hold |
| <a id="stop-manual-action"></a>`manual-action` | a search engine’s manual action notice on your Domain | `kill-switch` |
| <a id="stop-rival-row"></a>`rival-row` | two false rival rows in 30 days | You approve each one; each row is fixed within 24 h |
| <a id="stop-mention-complaint"></a>`mention-complaint` | two complaints in 30 days | mention requests pause; the author is suppressed |
| <a id="stop-mention-reply-floor"></a>`mention-reply-floor` | positive replies under 5 % after 60 requests | the Tactic stops |
| <a id="stop-kill-switch"></a>`kill-switch` | you press it | every Tactic pauses; published 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 second thread in Example.

```json title="The captured refusal · sample"
{
  "type": "https://startupbuilder.dev/errors/refused",
  "title": "Refused",
  "status": 422,
  "detail": "The comparison did not publish: a rival’s price row cites no capture, and their Pricing Page shows no price under 100 units.",
  "checks": [
    {
      "check": "fact-check",
      "status": "fail",
      "doc": "/plays/editorial-seo#check-fact-check",
      "fix": "Capture the rival’s own Page, or leave the row blank.",
      "then": "Content.draft"
    },
    {
      "check": "claims-resolve",
      "status": "pass"
    }
  ]
}
```


```json title="The Stops that fired, as Campaign.paused reasons · sample"
[
  {
    "reason": {
      "limit": "index",
      "observed": "4 of 11 Pages crawled, not indexed at 28 days (36 %)",
      "bound": "30 %",
      "window": "28d"
    },
    "doc": "/plays/editorial-seo#stop-index",
    "task": "task_576"
  }
]
```

## Did it work
The 24 kept families were assigned at random, 16 drafted and 8 held back, so both halves read the same assignment (rules m–o); each figure’s call is <Op verb="Metric.query" /> by node and by arm, and every node above the leaf is Not live yet (gaps #4).

`+118 visits per family a month · ■■□□ Early sign · n 24 families · path → arr`

| Node | Drafted half | Held-back half | Added | Confidence | Basis |
| --- | --- | --- | --- | --- | --- |
| `organic-visits` | 140 per family a month (2,240 over 16) | 22 (176 over 8) | +118 [+52, +184] | ■■□□ Early sign | held back at random · n 24 families · 5 Pages under 60 days still maturing |
| `icp-reached` | 84 ICP visitors per family a month | 14 | +70 [+18, +122] · carrying up | ■■□□ Early sign | the same assignment |
| `new-arr` ↑ | $2,082, 6 Customers on Pro · Booked: credited, not all caused by this Play | $347, 1 Customer (×2 for the half’s size: $694) | +$1,388 [−$700, +$3,500] · Worth about +$490 a month | ■□□□ Too early to tell (6 vs 1 Customers) | the same · 7 trials still deciding · Other Startups’ results +$410 a month per 4 Pages, ■■□□ Early sign, n 380 Startups |
| `net-new-arr` · `arr` | +$2,082 | +$694 | +$1,388, carried: it moves no other bridge term | ■□□□ Too early to tell | carried from `new-arr` |

The leaf counts organic visits per kept query family over 28 days, read monthly, to 20 families in each half; at 16 and 8, the Experiment runs on, and the held-back families draft at day 90.

**How it moves it:** each drafted family has a sourced guide or comparison of yours on its first results page; held-back families have none.

**Guardrails:** drafted against held back: trials per 100 ICP visitors, 1.4 against 1.3, and site-wide organic visits, 3,180 against a 28-day baseline of 2,950, so no `visibility` Stop.

```json title="The Campaign card · sample"
{
  "$id": "https://api.sb/leaseline.app/campaigns/editorial-seo",
  "$type": "Campaign",
  "play": "play:editorial-seo@1",
  "path": [
    "organic-visits",
    "icp-reached",
    "new-arr",
    "net-new-arr",
    "arr"
  ],
  "experiment": {
    "metric": "organic-visits",
    "unit": "per-family",
    "heldBack": 8,
    "decision": null
  },
  "readout": {
    "value": 118,
    "n": 24,
    "maturing": 5,
    "interval": [
      52,
      184
    ],
    "readability": "C",
    "basis": "held-back",
    "pooled": false
  },
  "ledger": {
    "spend": 2,
    "workerCost": 9,
    "hours": 4.1,
    "openArr": 2429,
    "wonArr": 2082,
    "projectedArr": 491,
    "paybackDays": 4
  }
}
```

Outside benchmark: clicks on the top result fell 58 % when an AI overview showed above it (a search-data vendor’s click-through study, in Your settings).

**Before it · After it.** Before it: `positioning-tournament` sets the words the Pages use. After it: `aeo` hears the Pages answer engines cite, `directory-listings` sends the comparers to your listings, and `instant-response` answers the readers who write in.

## Example
Leaseline ran it from 2026-08-24 to 2026-10-19 (56 days): 24 query families kept, 16 drafted and 8 held back at random; 16 Pages published, 14 indexed, 2 merged. Read every captured thread at `?threads=all`; three follow the tables.

| What happened, by option | Decisions | Published | Organic visits a month |
| --- | --- | --- | --- |
| Write a comparison | 7 | 7, 92 rows you approved | 1,210 |
| Write a guide · Refresh the Page | 9 · 4 | 9 · 4 | 1,030 · 3 Pages back in the top 5 |
| Merge or prune · Ask for a mention · Hide a disputed row | 2 · 6 · 1 | 2 redirects · 2 entries added · 1 row corrected | — |
| **Drafted half** · held-back half | **16 families** | **16 Pages** · 0 | **2,240** · 176 |

| Day | Event | What happened | Your time |
| --- | --- | --- | --- |
| 0 | `event_1470` Campaign.created | the three steps; the dry run fails `approved-pages` | 20 min |
| 0 | `event_1471` Site.published | the facts Page: Price, plans and 3 integrations, dated | 25 min |
| 1–14 | `event_1474` Approval.created, first of 7 | you approved 92 rival rows across 7 comparisons | 70 min |
| 12 | `event_1480` Page.indexed | the first Page indexed: How to send a 60-day renewal notice | — |
| 19 | `event_1486` Role.updated | promoted: guides and refreshes publish after a 1-hour hold | — |
| 28 · 34 | `event_1492` Change.merged · `event_1497` Campaign.paused | the 300 floor; `index`: 4 of 11 unindexed, resumed when 2 indexed and 2 merged | 15 min |
| 44 | `event_1503` Task.assigned | a rival disputed one row; hidden in 12 min, corrected by you the same day | 20 min |
| 1–56 | `event_1476` <Op verb="Task.next" />, weekly | the queue: 15 minutes a day in week 1, then about 10, from the <Op verb="Task.next" /> log | 1 h 45 min · **total 4.1 h measured, 4 h asked** |

> **Page, published Sep 6** · “How to send a 60-day renewal notice” · Published by Leaseline, which sells renewal software.¹ Across 3,520 renewals at 38 property managers, leases noticed 60+ days out renewed at 71 % (n 1,940).² Reviewed by Dana Okafor.³
>
> **Oct 19** · 410 organic visits in 28 days, position 3; 2 trials started from it.

```diff title="The alternatives comparison, 2026-09-18 · the draft fact-check refused, then the rewrite that published"
- Their plan costs about $1.50 a unit.
+ Their Pricing Page shows no price under 100 units (captured 2026-09-18), so this row is blank.
```

> **A rival’s marketing lead, 2026-10-07 09:40** · “Your row says we charge per unit. We don’t.” · The row was hidden at 09:52, and the Brief went to you (`task_581`), due 09:40 on Oct 8; **Dana, Oct 7, 15:10** · *checked their Pricing Page, corrected the row with a new capture, and replied.*⁴

¹ The first 60 words name the publisher and the seller. ² The number comes from your records, with its n. ³ The byline names the person who reviewed it. ⁴ A disputed row stays hidden until a person reviews it.

## Your settings
```mdx title="plays/editorial-seo.mdx"
---
holdout: 0.33   # 1 kept query family in 3 is held back at random
---
Write guides and honest comparisons for families of 300 searches a month or more. Dana approves every rival row.
```

```text
$ npx startup-builder show plays/editorial-seo
holdout 0.33 · floor 300 a month · rival rows to Dana     your file  plays/editorial-seo.mdx:2,4 (Ask 2026-08-24; line 4 by change_27, 2026-09-21)
family demand · process content-marketing · worker role:content-lead, owns never · chooses 6 (2 rule, 4 read) · sequence editorial-mention-request · approval every-send · checks owned-by-a-person, claims-resolve, fact-check, dispute-hold · stops 7 · placements google-search, bing, google-ai-overviews, chatgpt, gmail    catalog  play:editorial-seo@1 (Not live yet, gaps #5)
```

### Every key from the catalog
| Key | Id | What it is |
| --- | --- | --- |
| Tactic | <a id="tactic-editorial-backlog"></a>`editorial-backlog` | Editorial backlog ranks the comparison, alternative and guide query families a market searches and picks at most 3 a week to publish. Starts at Sends nothing. |
| Tactic | <a id="tactic-honest-compare"></a>`honest-compare` | Honest compare builds a comparison Page from captured rival sources and the claims source, with every row the rival wins and a stated method. Starts at You approve each one. |
| Tactic | <a id="tactic-grounded-guide"></a>`grounded-guide` | Grounded guide writes one guide Page per query family from a first-party measurement, with an artifact a summary cannot hold. Starts at You approve each one. |
| Tactic | <a id="tactic-editorial-publish-gate"></a>`editorial-publish-gate` | Editorial publish gate checks the copy rules, claims map, date, byline, duplicates, index rule and batch cap before a publish. Starts at Sends nothing. |
| Tactic | <a id="tactic-refresh-or-prune"></a>`refresh-or-prune` | Refresh or prune reads each Page’s fall against its own baseline, then refreshes it, holds it as a control, or prunes it. Starts at Sends nothing. |
| Tactic | <a id="tactic-page-mention-request"></a>`page-mention-request` | Page mention request asks the author of a ranking third-party comparison for a correction or an entry with dated sources, never a link. Starts at You approve each one. |
| Tactic | <a id="tactic-send-gate-check"></a>`send-gate-check` | Send gate check: Checks Suppression, a live reason, the Market, the basis stamp, the Person-month grant, the facts and the labels before any cold Message exists. Starts at Sends nothing. |
| Trigger | <a id="trigger-comparison-query-found"></a>`comparison-query-found` | Comparison query found: `threshold` on `Keyword.measured`, window 90d. |
| Trigger | <a id="trigger-rival-page-change"></a>`rival-page-change` | Rival page change: `event` on `Monitor.checked`, window 72h. |
| Trigger | <a id="trigger-page-fact-changed"></a>`page-fact-changed` | Page fact changed: `event` on `Fact.changed`, window 1d. |
| Trigger | <a id="trigger-page-rank-loss"></a>`page-rank-loss` | Page rank loss: `threshold` on `Keyword.measured`, window 14d. |
| Trigger | <a id="trigger-page-decline"></a>`page-decline` | Page decline: `threshold` on `Page.declined`, window 30d. |
| Trigger | <a id="trigger-page-not-indexed"></a>`page-not-indexed` | Page not indexed: `state` on `Monitor.matched`, window 60d. |
| Trigger | <a id="trigger-row-dispute"></a>`row-dispute` | Reading dispute: `event` on `Form.submitted`, window 2d. |
| Sequence | <a id="sequence-editorial-mention-request"></a>`editorial-mention-request` | Editorial mention request: 2 steps; it ends on A reply; An opt-out; A bounce; The inclusion or correction seen on the Page. |
| Benchmark | <a id="benchmark-self-promo-list-unrecommended"></a>`self-promo-list-unrecommended` | Share of cited self-promoting best-software lists whose owner the Google AI Overviews answer does not recommend: 69 % (Search Engine Journal analysis of Google AI Overviews, 2026-06-18). |
| Benchmark | <a id="benchmark-ai-overview-top-ctr-fall"></a>`ai-overview-top-ctr-fall` | Fall in position-1 click-through on keywords that show an AI overview, against a forecast.: 58 % (A search-data vendor’s click-through study., 2026-02-04). |
| Benchmark | <a id="benchmark-ai-summary-result-clicks"></a>`ai-summary-result-clicks` | Searches on which the user clicks any result while an AI summary shows: 8 % (Pew Research Center, 2025-07-22). |