AEO playbook skill
Portable across Claude Code, Codex, and Cursor. Reads local-CLI evidence. Does not open consumer LLM websites. Never --bare.
Full method: PLAYBOOK.md. Measurement spec: METHODOLOGY.md. Raw flags: aeo.
When to use
- "Which articles should we write?"
- "What do we do with this board?"
- "How do we close the loop after we ship?"
- "Pages are live, still zero mentions — write more?"
- "Do we need Search Console / Bing Webmaster?"
/aeo-playbook
Do not use this to run the full grid (that is aeo) or only to render a board (that is aeo-board). Use this to decide the next action.
Where the bits live
| Need |
Open |
| Calls / cells / ⚠ |
Board JSON |
Literal search_queries |
Evidence JSON (engines.*.search.search_queries) |
| Seed text |
Config or prompt_text on the evidence row |
| Whether our URL was cited |
raw_response_text (board does not extract citations) |
mention = whole-word brand/alias in answer text, not substring. A domain-style alias as an http(s) URL host counts; path-only tokens do not. recommended is the same bit. Board win is a ceiling (any engine, either arm) — still read the cells.
Next action
- If there is no current board,
python3 -m aeo board aeo-data/runs/<run_id>.json. Always pass the path. If there is no run, python3 -m aeo run --config aeo.config.json --class all --engine all --arm both --concurrency 4. One process; do not share --out across processes.
- Read the board JSON for calls (
win / gap / search-blind / trap) and cells. Do not narrate raw answers.
- Fan-out is not on the board. Open the evidence file and frequency-count
engines.*.search.search_queries. Drop hapaxes. Split confirmation (vendors_in_search_queries names an incumbent, not the brand) vs discovery (no configured vendor in the tool-call string). A confirmation name without the literal string is not a brief.
- Before blaming content: name the candidate URL from the sitemap (not a guessed seed id).
curl it. Reject homepage-sized 200s, homepage ETag, 308-to-/, .html hops, noindex, canonical=/. Then same-backend check: that engine's search arm (never --bare) with the literal fan-out string, plus site:<domain>/<path> and a branded H1 query. Homepage / llms.txt / GitHub do not count. Bing/Brave "fetched" or a VPS SERP scrape is not this check. If the URL is missing there, consoles + sitemap + IndexNow, then wait — do not draft. Full tree: PLAYBOOK.md §11.
- A page is allowed only if all three hold: repeating fan-out (or
gap + discovery); you looked at what those tool calls retrieve today; you can publish primary evidence this product can produce and will publish losses. One URL per cluster, not per string and not per ⚠ vendor. If a capability URL already answers it, edit that URL.
- No capture yet → stop. Freeze the protocol (inputs, versions, task list, content-addressed snapshot) first. Protocol matches the product. Do not invent a file-tree bake-off or MCP JSON for a product that does not ship those.
- After a ship and a same-backend check: re-run the affected roster ids with
--only-id (repeatable). --prompt-id is only a label for --prompt. Raise n with --samples 20 on that invocation only. n=20 × 3 engines × 2 arms = 120 cold starts per query. Use --concurrency on that one process if you parallelize cells.
- Keep the full roster. Do not drop watch queries. Do not inject the brand or incumbents into core seeds. Confirmation-probe satellites (
Foo vs Bar) are not briefs. Do not restart an in-flight full-grid run.
Calls → action
| Call |
Next action |
trap |
Keep measuring. No article. |
search-blind |
Do not mint a twin. EDIT the existing capability URL (H1 / FAQ / agent_prompt) so a later search matches training-weight phrasing. Changing the product or accepting weights is also valid. |
gap + confirmation |
Candidate for one compare URL against that incumbent, iff the string repeats and you can run a real head-to-head. Not one URL per ⚠ cell. |
gap + discovery |
Candidate for one category URL whose H1 is the dominant typed string, iff it repeats. |
win |
Do not mint a new URL. Check citations in raw_response_text. |
win means the brand string appeared (METHODOLOGY.md). It does not mean the right URL was cited.
- List URLs actually cited in
raw_response_text.
- Named the brand and cited a live canonical article you own → do not mint another URL. Re-run later.
- Named the brand and cited the homepage, a 308, a GitHub README, or nothing → deploy / index / fix that URL.
- Knowledge-only
win on a well-known brand is not a content brief.
Kill the page if
- You did not run both sides of the protocol you claimed.
- You hide a loss.
- The only win is a slogan.
- You could have written it from the homepage.
- The cluster is a surface you do not ship.
Anti-patterns (stop)
A mention after "this is an AEO eval" / reading ~/.aeo is contaminated — isolate cwd, Grok --sandbox strict. Injecting the brand into core seeds. Treating searched = false as a harness bug. Treating confirmation as discovery. One slug per ⚠ or per seed. A second dump. FAQPage / HowTo / star-rating JSON-LD. Logging consumer LLM accounts from a VPS. Using --prompt-id as a roster filter. Sharing --out across processes (use --concurrency instead).
Reason after the board
After a full-grid zero (or near-zero) mention, do not start with "write more articles." Long form: PLAYBOOK.md §9–10.
curl every URL you claim is live. Homepage-sized 200 or canonical=/ means the search backend cannot retrieve the page. Unpublished-branch markdown is invisible to the search arm.
- Knowledge-arm 0 on an unknown brand is expected year one. Keep measuring. Content budget = search-arm retrieval against incumbents the models already type.
- High search + 0 mentions is usually "URL not in that backend" or "they confirmed an incumbent and the incumbent's docs won" — not "mint dozens of slugs."
- Split cells: confirmation (vendor already in the tool-call string) vs discovery (category string, no configured vendor) vs search-blind (
searched=false). Confirmation needs a live compare URL that can beat the incumbent's own page on the same backend.
- Search-blind focus ids: EDIT existing H1 / FAQ /
agent_prompt. Do not mint twins.
- Map every seed to a shipped or in-PR slug before minting. If 100% cannibalize, new slugs only for clusters the tree does not own.
- One URL per cluster, not per seed and not per ⚠ vendor.
- Mention without search, or a transcript that says "this is an AEO eval," is contaminated. Isolate cwd. Do not publish that rate as a win.
- Nobody typing the brand into the search box is a first-class finding. Record it.
- Do not merge / deploy a dump of unpublished pages that 200 the homepage.
- If Wave 0 already passed and mentions are still 0, stop writing. Run PLAYBOOK.md §11 (live vs not-indexed vs skipped vs consensus). Verify Search Console + Bing Webmaster + IndexNow + Cloudflare AI Crawl Control. Re-check
site: before any new slug. If the compare is indexed and still loses to the incumbent's third-party docs, that is Gate E (off-site mention), not a new /answers twin.
Waves (unpublished PR + measured grid): 0 existing slugs 200 as themselves, sitemap, own canonical (merge-blocking; new drafts do not change AEO until then). If Wave 0 is already true and mentions stay 0, §11 — not Wave 2/3 content. 1 one compare URL per repeating confirmation incumbent you can bake off; shared frozen corpus; kill if the incumbent finds more in-family evidence. 2 EDIT existing slugs for search-blind / mismatched H1s; no new paths; no invented numbers. 3 remaining confirmation incumbents, then honesty pages only after capture; refuse surfaces you do not ship. If it could be written without running the product, it does not ship.
FAQ (short)
- Pages 200, still 0 mentions → more articles? No. §11. Index first.
- Guessed slug 404? Start from the sitemap /
*/index.json, not the seed id.
site: empty, homepage and llms.txt retrieve? Not indexed. Consoles + IndexNow. Wait.
site: hits the article, confirmation string still only the incumbent? Skipped. Edit that compare after a bake-off. No twin.
- Compare says "no benchmark was run"? Wave 0 ≠ Wave 1. Edit that URL after a frozen corpus.
- VPS Bing/Google HTML as proof? No. Datacenter scrapes lie. Use the cell's own search tool or Search Console coverage.
- "All the SEO"? Search Console + Bing Webmaster + sitemap submit + IndexNow. Stop until
site: returns the slugs.
- Cloudflare / GPTBot?
robots.txt Allow: / is not enough if CF "Block AI bots" is on. curl -A OAI-SearchBot / ChatGPT-User / Claude-SearchBot / PerplexityBot. Open the dashboard.
- YouTube / Ahrefs 0.737 / Brand Radar? Off-site mention is Gate E after the compare already loses. Not a reason to mint slugs, rewrite seeds, or measure ChatGPT web / AI Overviews with this skill.
- Mention after "this is an AEO eval"? Contaminated. Isolate cwd. Do not publish the rate.
Write-up checklist
A run summary for humans is not done if any item is missing. Recompute every number from evidence JSON + live curl the same day. Do not invent a number — say the file was not opened.
- Method (two-arm, verbatim seeds, isolate cwd, engines, n).
- Live URL check of claimed pages + sitemap (status, bytes vs homepage, canonical). If that already passes, Gate B (
site: / branded / literal fan-out), consoles + IndexNow + CF AI-bot status, and whether Gate E (consensus) applies (PLAYBOOK.md §11).
- Mention / search / prebelief per engine × arm. Isolate contaminated cells; do not blend them into the win rate.
- Confirmation vs discovery counts and vendor fan-out (
search_queries / vendors_in_search_queries), including "typed brand into the box?"
- Search-blind focus ids (verbatim seeds).
- Cannibalize / coverage map: seeds → existing slugs. New slugs only if the tree does not own the cluster.
- Calls → next action (
trap / search-blind / gap+confirmation / gap+discovery / win).
- Wave plan + merge/deploy gates + kill rules + refuse list with reasons.
- Safe product claims vs claims you will not make.
- What you will re-run (
--only-id) after ship, and what you will not (do not restart a full grid).
Human view of the same payload: python3 -m aeo board <file> (markdown + JSON; --format html writes the standalone report) plus the evidence JSON. Merge engine files with python3 -m aeo report --html --out report.html run-a.json run-b.json.
Testimony judge
After a full evidence run, scripts/judge_run.py classifies each brand_mentioned cell (stance/position/quote) and extracts named vendors from every completed arm. Config competitors is the seed / known set. A miss that only names UserCheck still shows up as a surprise (not on the seed list). The board judge is fed high-frequency surprises as a gap. Then scripts/render_judge_html.py. --vendors-only skips stance. Do not treat CLI recommended as testimony. Grok AEO runs must use GROK_HOME without MCP and may need GROK_SANDBOX=workspace when Docker Desktop makes docker.sock a symlink.
1---2name: aeo-playbook3description: Use when planning AEO content, turning a board into article briefs, running a measure→ship→re-run loop, or debugging why live pages still get zero mentions (retrieval / Search Console). Complements the aeo measurement skill and the aeo-board skill.4---56# AEO playbook skill78Portable across Claude Code, Codex, and Cursor. Reads local-CLI evidence. Does **not** open consumer LLM websites. Never `--bare`.910Full method: [PLAYBOOK.md](../../PLAYBOOK.md). Measurement spec: [METHODOLOGY.md](../../METHODOLOGY.md). Raw flags: [aeo](../aeo/SKILL.md).1112## When to use1314- "Which articles should we write?"15- "What do we do with this board?"16- "How do we close the loop after we ship?"17- "Pages are live, still zero mentions — write more?"18- "Do we need Search Console / Bing Webmaster?"19- `/aeo-playbook`2021Do not use this to run the full grid (that is [aeo](../aeo/SKILL.md)) or only to render a board (that is [aeo-board](../aeo-board/SKILL.md)). Use this to decide the next action.2223## Where the bits live2425| Need | Open |26| --- | --- |27| Calls / cells / ⚠ | Board JSON |28| Literal `search_queries` | Evidence JSON (`engines.*.search.search_queries`) |29| Seed text | Config or `prompt_text` on the evidence row |30| Whether our URL was cited | `raw_response_text` (board does not extract citations) |3132`mention` = whole-word brand/alias in answer text, not substring. A domain-style alias as an http(s) URL host counts; path-only tokens do not. `recommended` is the same bit. Board `win` is a ceiling (any engine, either arm) — still read the cells.3334## Next action35361. If there is no current board, `python3 -m aeo board aeo-data/runs/<run_id>.json`. Always pass the path. If there is no run, `python3 -m aeo run --config aeo.config.json --class all --engine all --arm both --concurrency 4`. One process; do not share `--out` across processes.372. Read the board JSON for calls (`win` / `gap` / `search-blind` / `trap`) and cells. Do not narrate raw answers.383. Fan-out is **not** on the board. Open the evidence file and frequency-count `engines.*.search.search_queries`. Drop hapaxes. Split confirmation (`vendors_in_search_queries` names an incumbent, not the brand) vs discovery (no configured vendor in the tool-call string). A confirmation *name* without the literal string is not a brief.394. Before blaming content: name the candidate URL from the **sitemap** (not a guessed seed id). `curl` it. Reject homepage-sized 200s, homepage ETag, 308-to-`/`, `.html` hops, `noindex`, canonical=`/`. Then same-backend check: that engine's search arm (never `--bare`) with the **literal** fan-out string, plus `site:<domain>/<path>` and a branded H1 query. Homepage / `llms.txt` / GitHub do not count. Bing/Brave "fetched" or a VPS SERP scrape is not this check. If the URL is missing there, consoles + sitemap + IndexNow, then wait — do not draft. Full tree: [PLAYBOOK.md](../../PLAYBOOK.md) §11.405. A page is allowed only if all three hold: repeating fan-out (or `gap` + discovery); you looked at what those tool calls retrieve today; you can publish primary evidence **this product can produce** and will publish losses. One URL per cluster, not per string and not per ⚠ vendor. If a capability URL already answers it, edit that URL.416. No capture yet → stop. Freeze the protocol (inputs, versions, task list, content-addressed snapshot) first. Protocol matches the product. Do not invent a file-tree bake-off or MCP JSON for a product that does not ship those.427. After a ship and a same-backend check: re-run the affected **roster ids** with `--only-id` (repeatable). `--prompt-id` is only a label for `--prompt`. Raise n with `--samples 20` on that invocation only. n=20 × 3 engines × 2 arms = 120 cold starts per query. Use `--concurrency` on that one process if you parallelize cells.438. Keep the full roster. Do not drop watch queries. Do not inject the brand or incumbents into core seeds. Confirmation-probe satellites (`Foo vs Bar`) are not briefs. Do not restart an in-flight full-grid run.4445## Calls → action4647| Call | Next action |48| --- | --- |49| `trap` | Keep measuring. No article. |50| `search-blind` | Do not mint a twin. EDIT the existing capability URL (H1 / FAQ / `agent_prompt`) so a later search matches training-weight phrasing. Changing the product or accepting weights is also valid. |51| `gap` + confirmation | *Candidate* for one compare URL against that incumbent, iff the string repeats and you can run a real head-to-head. Not one URL per ⚠ cell. |52| `gap` + discovery | *Candidate* for one category URL whose H1 is the dominant typed string, iff it repeats. |53| `win` | Do not mint a new URL. Check citations in `raw_response_text`. |5455`win` means the brand string appeared (METHODOLOGY.md). It does not mean the right URL was cited.56571. List URLs actually cited in `raw_response_text`.582. Named the brand and cited a live canonical article you own → do not mint another URL. Re-run later.593. Named the brand and cited the homepage, a 308, a GitHub README, or nothing → deploy / index / fix that URL.604. Knowledge-only `win` on a well-known brand is not a content brief.6162## Kill the page if6364- You did not run both sides of the protocol you claimed.65- You hide a loss.66- The only win is a slogan.67- You could have written it from the homepage.68- The cluster is a surface you do not ship.6970## Anti-patterns (stop)7172A mention after "this is an AEO eval" / reading `~/.aeo` is contaminated — isolate cwd, Grok `--sandbox strict`. Injecting the brand into core seeds. Treating `searched = false` as a harness bug. Treating confirmation as discovery. One slug per ⚠ or per seed. A second dump. `FAQPage` / `HowTo` / star-rating JSON-LD. Logging consumer LLM accounts from a VPS. Using `--prompt-id` as a roster filter. Sharing `--out` across processes (use `--concurrency` instead).7374## Reason after the board7576After a full-grid zero (or near-zero) mention, do **not** start with "write more articles." Long form: [PLAYBOOK.md](../../PLAYBOOK.md) §9–10.77781. `curl` every URL you claim is live. Homepage-sized 200 or canonical=`/` means the search backend cannot retrieve the page. Unpublished-branch markdown is invisible to the search arm.792. Knowledge-arm 0 on an unknown brand is expected year one. Keep measuring. Content budget = search-arm retrieval against incumbents the models already type.803. High search + 0 mentions is usually "URL not in that backend" or "they confirmed an incumbent and the incumbent's docs won" — not "mint dozens of slugs."814. Split cells: confirmation (vendor already in the tool-call string) vs discovery (category string, no configured vendor) vs search-blind (`searched=false`). Confirmation needs a live compare URL that can beat the incumbent's own page on the *same* backend.825. Search-blind focus ids: EDIT existing H1 / FAQ / `agent_prompt`. Do not mint twins.836. Map every seed to a shipped or in-PR slug before minting. If 100% cannibalize, new slugs only for clusters the tree does not own.847. One URL per cluster, not per seed and not per ⚠ vendor.858. Mention without search, or a transcript that says "this is an AEO eval," is contaminated. Isolate cwd. Do not publish that rate as a win.869. Nobody typing the brand into the search box is a first-class finding. Record it.8710. Do not merge / deploy a dump of unpublished pages that 200 the homepage.8811. If Wave 0 already passed and mentions are still 0, **stop writing**. Run [PLAYBOOK.md](../../PLAYBOOK.md) §11 (live vs not-indexed vs skipped vs consensus). Verify Search Console + Bing Webmaster + IndexNow + Cloudflare AI Crawl Control. Re-check `site:` before any new slug. If the compare is indexed and still loses to the incumbent's third-party docs, that is Gate E (off-site mention), not a new `/answers` twin.8990Waves (unpublished PR + measured grid): **0** existing slugs 200 as themselves, sitemap, own canonical (merge-blocking; new drafts do not change AEO until then). If Wave 0 is already true and mentions stay 0, §11 — not Wave 2/3 content. **1** one compare URL per repeating confirmation incumbent you can bake off; shared frozen corpus; kill if the incumbent finds more in-family evidence. **2** EDIT existing slugs for search-blind / mismatched H1s; no new paths; no invented numbers. **3** remaining confirmation incumbents, then honesty pages only after capture; refuse surfaces you do not ship. If it could be written without running the product, it does not ship.9192## FAQ (short)9394- **Pages 200, still 0 mentions → more articles?** No. §11. Index first.95- **Guessed slug 404?** Start from the sitemap / `*/index.json`, not the seed id.96- **`site:` empty, homepage and `llms.txt` retrieve?** Not indexed. Consoles + IndexNow. Wait.97- **`site:` hits the article, confirmation string still only the incumbent?** Skipped. Edit that compare after a bake-off. No twin.98- **Compare says "no benchmark was run"?** Wave 0 ≠ Wave 1. Edit that URL after a frozen corpus.99- **VPS Bing/Google HTML as proof?** No. Datacenter scrapes lie. Use the cell's own search tool or Search Console coverage.100- **"All the SEO"?** Search Console + Bing Webmaster + sitemap submit + IndexNow. Stop until `site:` returns the slugs.101- **Cloudflare / GPTBot?** `robots.txt` `Allow: /` is not enough if CF "Block AI bots" is on. `curl -A` `OAI-SearchBot` / `ChatGPT-User` / `Claude-SearchBot` / `PerplexityBot`. Open the dashboard.102- **YouTube / Ahrefs 0.737 / Brand Radar?** Off-site mention is Gate E after the compare already loses. Not a reason to mint slugs, rewrite seeds, or measure ChatGPT web / AI Overviews with this skill.103- **Mention after "this is an AEO eval"?** Contaminated. Isolate cwd. Do not publish the rate.104105## Write-up checklist106107A run summary for humans is not done if any item is missing. Recompute every number from evidence JSON + live `curl` the same day. Do not invent a number — say the file was not opened.1081091. Method (two-arm, verbatim seeds, isolate cwd, engines, n).1102. Live URL check of claimed pages + sitemap (status, bytes vs homepage, canonical). If that already passes, Gate B (`site:` / branded / literal fan-out), consoles + IndexNow + CF AI-bot status, and whether Gate E (consensus) applies ([PLAYBOOK.md](../../PLAYBOOK.md) §11).1113. Mention / search / prebelief per engine × arm. Isolate contaminated cells; do not blend them into the win rate.1124. Confirmation vs discovery counts and vendor fan-out (`search_queries` / `vendors_in_search_queries`), including "typed brand into the box?"1135. Search-blind focus ids (verbatim seeds).1146. Cannibalize / coverage map: seeds → existing slugs. New slugs only if the tree does not own the cluster.1157. Calls → next action (`trap` / `search-blind` / `gap`+confirmation / `gap`+discovery / `win`).1168. Wave plan + merge/deploy gates + kill rules + refuse list with reasons.1179. Safe product claims vs claims you will not make.11810. What you will re-run (`--only-id`) after ship, and what you will not (do not restart a full grid).119120Human view of the same payload: `python3 -m aeo board <file>` (markdown + JSON; `--format html` writes the standalone report) plus the evidence JSON. Merge engine files with `python3 -m aeo report --html --out report.html run-a.json run-b.json`.121122123## Testimony judge124125After a full evidence run, `scripts/judge_run.py` classifies each `brand_mentioned` cell (stance/position/quote) **and** extracts named vendors from every completed arm. Config `competitors` is the seed / known set. A miss that only names UserCheck still shows up as a **surprise** (not on the seed list). The board judge is fed high-frequency surprises as a gap. Then `scripts/render_judge_html.py`. `--vendors-only` skips stance. Do not treat CLI `recommended` as testimony. Grok AEO runs must use `GROK_HOME` without MCP and may need `GROK_SANDBOX=workspace` when Docker Desktop makes `docker.sock` a symlink.