Skip to content

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.

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

Hand our latest search finding to the Content Lead agent and show me what it chose and why. Don’t send.

curl "https://api.sb/leaseline.app/events?where=name%20in%20(%22Keyword.measured%22%2C%20%22Monitor.checked%22%2C%20%22Fact.changed%22%2C%20%22Page.declined%22%2C%20%22Monitor.matched%22%2C%20%22Form.submitted%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_1512"}'

# Not live yet (gaps #7)
curl "https://api.sb/leaseline.app/decisions?where=on%20%3D%20%22keyword_44%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%22keyword_44%22" \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY"
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 }
npx startup-builder do Event.list --json '{"where":"name in (\"Keyword.measured\", \"Monitor.checked\", \"Fact.changed\", \"Page.declined\", \"Monitor.matched\", \"Form.submitted\")","limit":1}'
npx startup-builder do Trigger.fire --json '{"like":"event_1512"}' --try   # Not live yet (gaps #7)
npx startup-builder do Decision.list --json '{"where":"on = \"keyword_44\""}'   # Not live yet (gaps #7)
npx startup-builder do Action.list --json '{"where":"on = \"keyword_44\""}'   # Not live yet (gaps #7)
{
  "name": "do",
  "arguments": {
    "code": "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\nconst { subject } = await $.Trigger.fire({ like: last.$id }, { mode: 'try' })   // the same Event again, in try mode · Not live yet, gaps #7\nconst { decisions: [decision] } = await $.Decision.list({ where: `on = \"${subject}\"` })   // what it chose, and why · Not live yet, gaps #7\nconst { actions } = await $.Action.list({ where: `on = \"${subject}\"` })   // what it did, with its checks · Not live yet, gaps #7\nreturn { decision, actions }",
    "mode": "try"
  }
}
Response200application/json
{
  "decision": {
    "$id": "https://api.sb/leaseline.app/decisions/decision_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"
  …
sample · 4 Not live yetPreview

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

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 familyChoiceReason
Alternatives to the largest suite, for under 100 units · 2,400 a month · no Pagewrite-a-comparisonover the 300 floor; 14 rival rows go to you
How to send a 60-day renewal notice · 880 a monthwrite-a-guideyour records answer it: 3,520 renewals
Guide “Lease renewal letter template” dropped 4 positions on two weekly readsrefresh-the-pageits example still shows a 30-day notice; the template changed Oct 2
Guide on move-out checklists: never indexed after 90 daysmerge-or-prunethe 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-rowa written dispute hides the row within 24 h until you review it

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

Terminal
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"}'
422Refusededitorial-seo can’t start yet: 1 check fails and 1 warns.
CheckFixThen
use-when pass · Not live yet, gaps #8None needed: 3 families are enough.POST /{namespace}/metrics/query
baseline pass · Not live yet, gaps #8None needed: 1,240 in 90 days is your Before.POST /{namespace}/metrics/query
approved-pages fail · Not live yet, gaps #8Publish dated fact records for Price, plans and 3 integrations before a row cites them.POST /{namespace}/sites/{site}/publish
role-unstaffed warn · Not live yet, gaps #8Staff the Content Lead, or leave it: every rival row comes to you.POST /{namespace}/roles/{role}/staff
JSON
{
  "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"
    }
  ]
}

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

CheckFix (one call)Your timeWait
use-when · baseline · pass: 22 families with no Page; 1,240 organic visits in 90 days (gaps #8)POST /{namespace}/metrics/query on organic-visits, which fills Before in Your number1 minnone
approved-pages · fail (Not live yet)POST /{namespace}/sites/{site}/publish the facts Page with Price, plans and 3 integrations25 minnone
role-unstaffed · warn (gaps #8): no Content Lead seatPOST /{namespace}/roles/{role}/staff, or leave it: every rival row comes to you0 minnone

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

WhenTrigger idStarts onWindowHow it gets in
A searched query family clears the floor with no Page of yourscomparison-query-foundthreshold on Keyword.measured where family.volume >= demand floor and no Page targets itread weekly; 90dPOST /{namespace}/keywords for each family; POST /{namespace}/keywords/{keyword}/measure reads it weekly
A Page drops 3+ positions on two weekly readspage-rank-lossthreshold on Keyword.measured where position drop >= 3 on 2 weekly reads in a row14dthe same weekly POST /{namespace}/keywords/{keyword}/measure
+ 3 more for changed facts, slipping Pages and Pages never indexed (?lanes=all)
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
    }
  }
}
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.

OptionRung todayEvidence (passed of n)Moves up atDrops back whenYou can
Write a guide · Refresh the PageIt does it, you can stop it9 of 9 · 4 of 4met on day 19 (the first 5 guides)a published sentence fails claims-resolve → You approve each onePOST /{namespace}/actions/{action}/cancel inside the hold
Write a comparison · Hide a disputed rowYou approve each one7 of 7, 92 rows · 1 of 1never: rival rows stay with you—POST /{namespace}/approvals/{approval}/approve · POST /{namespace}/tasks/{task}/claim
Merge or prune · Ask for a mentionIt does it2 of 2 · 6 of 6—a mention complaint → pausedPOST /{namespace}/actions/{action}/redirect
Change the backlog ruleYou approve each one1 merged of 1never—POST /{namespace}/changes/{change}/merge · POST /{namespace}/changes/{change}/revert
Never: a ranked list with you in it, an estimated rival price, a date moved with no fact changed, an invented bylineTo a person, every time0 askednever—POST /{namespace}/tasks/{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 POST /{namespace}/content/draft, POST /{namespace}/approvals, POST /{namespace}/content/{content}/publish, PATCH /{namespace}/content/{id}, PATCH /{namespace}/pages/{id}, POST /{namespace}/messages/{message}/schedule, POST /{namespace}/tasks/{task}/assign, and any other call becomes an Approval under the Role’s Mandate.

The brief (folded): compiled Agent-register Markdown · play: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 POST /{namespace}/changes/{change}/merge. Replayed on past reads, POST /{namespace}/changes/{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; POST /{namespace}/roles/{role}/staff seats it.

Terminal
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

OptionPicked whenDecided byRunsHands toIf it’s just you
Write a comparison write-a-comparisona versus, alternatives or rival-pair family clears the floorYour rule (line 4)publish-content → POST /{namespace}/content/draft with captures → POST /{namespace}/approvals for the rowsPOST /{namespace}/tasks/{task}/assign → you, due 1 business day · Take this oneyou
Write a guide write-a-guidea how-to family clears the floor, and your records answer itAgent’s call, with its reasonpublish-content → POST /{namespace}/content/draft → POST /{namespace}/content/{content}/publishnobodythe same
Refresh the Page refresh-the-pagea cited fact or rival Page changed, or a Page slippedAgent’s call, with its reasonPATCH /{namespace}/content/{id} → POST /{namespace}/content/{content}/publishnobodythe same
Merge or prune merge-or-prunenever indexed after 90 days, or another Page answers itAgent’s call, with its reasonPATCH /{namespace}/pages/{id} to the nearest Page, behind a redirectnobodythe same
Ask for a mention ask-for-a-mentiona ranking Page misstates you or leaves you outAgent’s call, with its reasoneditorial-mention-request → POST /{namespace}/messages/{message}/schedulethe Page’s authorthe same
Hide a disputed row hide-a-disputed-rowa rival disputes a row in writingYour rule (the dispute hold)PATCH /{namespace}/pages/{id} hides the row → POST /{namespace}/tasks/{task}/assignyou review it, due 1 business day · Take this oneyou
When they write backFixture replyWhat happensWho answersBy whenCall
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 ownContent Lead · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Send me pricing“What does Leaseline cost at 40 units?”answers with the Pricing Page’s list Price, $29 a monthContent Lead · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Not now“We revise that list in January.”the follow-up stops; the Decision reopens at their revisionnobodyat oncePOST /{namespace}/enrollments/{enrollment}/cancel
Wrong person“Our editor handles that list.”asks them to forward it; it never writes to an address from their textContent Lead · your agent1 business dayPOST /{namespace}/conversations/{conversation}/answer
Out of officean auto-reply until Oct 28the follow-up waits for the return datenobodythe return datePOST /{namespace}/messages/{message}/schedule
Question it can’t answer“Would you pay for a placement?”To a person: paid placement is your call, and it carries their sponsored labelyou1 business dayPOST /{namespace}/conversations/{conversation}/escalate
Stop“Please don’t email us again.”that author is suppressed for all your Startupsnobodyat oncePOST /{namespace}/contacts/{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).

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 POST /{namespace}/tasks/{task}/claim (Take this one), POST /{namespace}/tasks/{task}/return with a reason, POST /{namespace}/approvals/{approval}/approve or POST /{namespace}/approvals/{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 POST /{namespace}/tasks/{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: POST /{namespace}/deals/{deal}/assign sets owner to the receiving person, the agent is never a Deal’s owner, and Meetings land on the owner’s calendar under the owner’s name.

When it won’t send

Check (refuses one send)RefusesFixThen
owned-by-a-person (rule j; Not live yet, gaps #8)a mention request to an author whose Conversation a person claimedLeave it to the person who owns it: the draft waits in their queue.POST /{namespace}/tasks/{task}/assign
claims-resolve (shared)a sentence the claims source or a rival capture lacksCut the claim, or name the record that backs it.POST /{namespace}/content/draft
fact-check (shared)a rival row with no capture, or a price the rival does not publishCapture the rival’s own Page, or leave the row blank.POST /{namespace}/content/draft
dispute-hold (the record’s Dispute hold)a row a rival disputed in writing, until you review itReview it with a fresh capture; it stays hidden meanwhile.POST /{namespace}/tasks/{task}/claim
Stop (pauses the Play)Pauses whenThen
visibilitysite-wide organic visits 25 % under the 28-day baseline within 14 days of a search updatenew publishes pause; You approve each one
indexover 30 % of the Play’s Pages crawled, not indexed, at 28 daysnew publishes hold
manual-actiona search engine’s manual action notice on your Domainkill-switch
rival-rowtwo false rival rows in 30 daysYou approve each one; each row is fixed within 24 h
mention-complainttwo complaints in 30 daysmention requests pause; the author is suppressed
mention-reply-floorpositive replies under 5 % after 60 requeststhe Tactic stops
kill-switchyou press itevery 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.

422RefusedThe comparison did not publish: a rival’s price row cites no capture, and their Pricing Page shows no price under 100 units.
CheckFixThen
fact-check fail · Not live yet, gaps #1Capture the rival’s own Page, or leave the row blank.POST /{namespace}/content/draft
claims-resolve pass—POST /{namespace}/content/draft
JSON
{
  "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"
    }
  ]
}

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

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 POST /{namespace}/metrics/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

Chose
write-a-comparison
Why
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
From
write-a-comparison · write-a-guide · refresh-the-page · merge-or-prune · ask-for-a-mention · hide-a-disputed-row
Expects
the Page indexed within 28d, and organic visits past 100 a month by day 60
Reopens when
Monitor.matched page-not-indexed for keyword_44’s Page within 28d
By
worker_7
JSON
{
  "$id": "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"
}
NodeDrafted halfHeld-back halfAddedConfidenceBasis
organic-visits140 per family a month (2,240 over 16)22 (176 over 8)+118 [+52, +184]■■□□ Early signheld back at random · n 24 families · 5 Pages under 60 days still maturing
icp-reached84 ICP visitors per family a month14+70 [+18, +122] · carrying up■■□□ Early signthe 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 tellcarried 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.

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 optionDecisionsPublishedOrganic visits a month
Write a comparison77, 92 rows you approved1,210
Write a guide · Refresh the Page9 · 49 · 41,030 · 3 Pages back in the top 5
Merge or prune · Ask for a mention · Hide a disputed row2 · 6 · 12 redirects · 2 entries added · 1 row corrected—
Drafted half · held-back half16 families16 Pages · 02,240 · 176
DayEventWhat happenedYour time
0event_1470 Campaign.createdthe three steps; the dry run fails approved-pages20 min
0event_1471 Site.publishedthe facts Page: Price, plans and 3 integrations, dated25 min
1–14event_1474 Approval.created, first of 7you approved 92 rival rows across 7 comparisons70 min
12event_1480 Page.indexedthe first Page indexed: How to send a 60-day renewal notice—
19event_1486 Role.updatedpromoted: guides and refreshes publish after a 1-hour hold—
28 · 34event_1492 Change.merged · event_1497 Campaign.pausedthe 300 floor; index: 4 of 11 unindexed, resumed when 2 indexed and 2 merged15 min
44event_1503 Task.assigneda rival disputed one row; hidden in 12 min, corrected by you the same day20 min
1–56event_1476 POST /{namespace}/tasks/next, weeklythe queue: 15 minutes a day in week 1, then about 10, from the POST /{namespace}/tasks/next log1 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.

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

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

KeyIdWhat it is
Tacticeditorial-backlogEditorial 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.
Tactichonest-compareHonest 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.
Tacticgrounded-guideGrounded 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.
Tacticeditorial-publish-gateEditorial publish gate checks the copy rules, claims map, date, byline, duplicates, index rule and batch cap before a publish. Starts at Sends nothing.
Tacticrefresh-or-pruneRefresh 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.
Tacticpage-mention-requestPage 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.
Tacticsend-gate-checkSend 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.
Triggercomparison-query-foundComparison query found: threshold on Keyword.measured, window 90d.
Triggerrival-page-changeRival page change: event on Monitor.checked, window 72h.
Triggerpage-fact-changedPage fact changed: event on Fact.changed, window 1d.
Triggerpage-rank-lossPage rank loss: threshold on Keyword.measured, window 14d.
Triggerpage-declinePage decline: threshold on Page.declined, window 30d.
Triggerpage-not-indexedPage not indexed: state on Monitor.matched, window 60d.
Triggerrow-disputeReading dispute: event on Form.submitted, window 2d.
Sequenceeditorial-mention-requestEditorial mention request: 2 steps; it ends on A reply; An opt-out; A bounce; The inclusion or correction seen on the Page.
Benchmarkself-promo-list-unrecommendedShare 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).
Benchmarkai-overview-top-ctr-fallFall 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).
Benchmarkai-summary-result-clicksSearches on which the user clicks any result while an AI summary shows: 8 % (Pew Research Center, 2025-07-22).

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