OpenSEO Keyword Research
Turn seed topics into a prioritized, MEASURED keyword opportunity set and land it in brand/keyword-plan.md. mktg's keyword-research is the playbook (methodology); this skill is the measured-data engine behind it when OpenSEO is configured.
On Activation
- Catalog check:
mktg catalog info openseo --json --fields configured — if not configured, stop spending: state the gap and hand off to Exa-backed keyword-research with metrics marked unknown.
- Project binding: read
.seo/openseo.json for projectId. Missing → run openseo-project-setup first (or ask the user for the project id).
- Brand grounding: read
brand/positioning.md + existing brand/keyword-plan.md (tolerate templates). Business-fit beats volume-fit — the positioning file is the filter.
OpenSEO MCP Tools
get_search_console_performance: when GSC is connected, START here. High rowLimit, filter average position ~5–20 client-side (the API sorts by clicks, not position). These striking-distance terms are the fastest wins — and zero extra credit cost.
get_keyword_metrics: hydrate up to 700 keywords per call with volume, KD, intent, CPC, trends. Use on striking-distance terms and every candidate set.
research_keywords: discovery from 1–5 seeds per call; prefer ~150 results unless exhaustive research was requested.
get_ranked_keywords: when the brief includes a domain/page — exact ranking rows (near-misses, competitor-owned terms).
get_serp_results: inspect SERPs for top candidates when intent is ambiguous. Keep batches small (≤10 queries).
list_saved_keywords: avoid re-researching what's already saved.
save_keywords: ONLY after explicit user confirmation, with concise tags (topic:<t>, intent:<i>, page:<slug>).
Workflow
- Normalize seeds into 2–5 distinct research angles filtered by positioning.
- GSC connected? Pull striking-distance terms first and hydrate with
get_keyword_metrics. Work that list before broad discovery.
research_keywords per angle; get_keyword_metrics to hydrate; get_ranked_keywords if a domain is in the brief.
- Remove irrelevant, duplicate, branded-only, and off-intent terms.
- Prioritize by practical opportunity: business fit → clear intent → reasonable KD → volume/CPC signal → winnable SERP.
get_serp_results for high-potential or ambiguous terms when SERP intent would change the call.
- Write the shortlist into
brand/keyword-plan.md (preserve its required sections per brand/SCHEMA.md; confirm before overwriting populated sections).
- Present: best opportunity theme, top keywords now, keywords to save, SERP caveats. Then next actions:
openseo-keyword-clustering, seo-content, or save.
Cost Discipline
- State estimated call counts before bulk pulls (>200 keywords) and get confirmation.
- Small exploratory batches are fine without asking.
- GSC-first ordering exists precisely to avoid spending credits on data the user already owns.
Anti-Patterns
- Inventing metrics when OpenSEO returns nothing — because a hallucinated KD of "about 35" silently becomes the foundation of a content plan. If OpenSEO doesn't return a value, write
unknown.
- Volume-first prioritization — because a 10k-volume term that doesn't match the product converts nobody and burns months. Positioning filters the list before metrics rank it.
save_keywords without explicit confirmation — because saves mutate the user's OpenSEO account and bulk saves burn credit. Ask, state the count, then save.
- Skipping the GSC-first pass when GSC is connected — because striking-distance terms (positions 5–20) are provably the cheapest wins in SEO and they're free to read. Discovery research before first-party data is wasted spend.
- Overwriting a populated
keyword-plan.md without confirmation — because that file is brand memory other skills build on (seo-content, seo-machine). Merge; confirm destructive rewrites.
Close the loop
After writing files, log completion so mktg plan / mktg status count the work (bare mktg run only logs loaded):
mktg run openseo-keyword-research --complete --writes <paths written> --result success --json
Progressive Enhancement
| Level |
Behavior |
| L0 (no OpenSEO) |
Hand off to Exa-backed keyword-research; metrics unknown |
L1 (OPENSEO_API_KEY) |
Metrics via available calls; MCP steps deferred |
| L2 (MCP connected) |
Full workflow incl. SERP validation |
| L3 (GSC connected) |
Striking-distance-first ordering; highest-signal path |
Adapted from every-app/open-seo .agents/skills/keyword-research (MIT). Workflow and tool guidance upstream; mktg brand-memory writes, positioning filter, and cost discipline added here.
1---2name: openseo-keyword-research3description: Discover keyword opportunities with MEASURED volume, keyword difficulty, CPC, and intent from OpenSEO, then write them into brand/keyword-plan.md. Use this skill whenever someone asks for keyword difficulty, KD, search volume, keyword ideas with metrics, striking-distance opportunities from Search Console, or SERP-validated keyword priorities. For qualitative research without an OpenSEO connection, use mktg's keyword-research instead (metrics will be unknown). Triggers: "keyword difficulty", "search volume", "keyword opportunities", "striking distance keywords", "measured keyword research".4---56# OpenSEO Keyword Research78Turn seed topics into a prioritized, MEASURED keyword opportunity set and land it in `brand/keyword-plan.md`. mktg's `keyword-research` is the playbook (methodology); this skill is the measured-data engine behind it when OpenSEO is configured.910## On Activation11121. **Catalog check**: `mktg catalog info openseo --json --fields configured` — if not configured, stop spending: state the gap and hand off to Exa-backed `keyword-research` with metrics marked `unknown`.132. **Project binding**: read `.seo/openseo.json` for `projectId`. Missing → run `openseo-project-setup` first (or ask the user for the project id).143. **Brand grounding**: read `brand/positioning.md` + existing `brand/keyword-plan.md` (tolerate templates). Business-fit beats volume-fit — the positioning file is the filter.1516## OpenSEO MCP Tools1718- `get_search_console_performance`: when GSC is connected, START here. High `rowLimit`, filter average position ~5–20 client-side (the API sorts by clicks, not position). These striking-distance terms are the fastest wins — and zero extra credit cost.19- `get_keyword_metrics`: hydrate up to 700 keywords per call with volume, KD, intent, CPC, trends. Use on striking-distance terms and every candidate set.20- `research_keywords`: discovery from 1–5 seeds per call; prefer ~150 results unless exhaustive research was requested.21- `get_ranked_keywords`: when the brief includes a domain/page — exact ranking rows (near-misses, competitor-owned terms).22- `get_serp_results`: inspect SERPs for top candidates when intent is ambiguous. Keep batches small (≤10 queries).23- `list_saved_keywords`: avoid re-researching what's already saved.24- `save_keywords`: ONLY after explicit user confirmation, with concise tags (`topic:<t>`, `intent:<i>`, `page:<slug>`).2526## Workflow27281. Normalize seeds into 2–5 distinct research angles filtered by positioning.292. GSC connected? Pull striking-distance terms first and hydrate with `get_keyword_metrics`. Work that list before broad discovery.303. `research_keywords` per angle; `get_keyword_metrics` to hydrate; `get_ranked_keywords` if a domain is in the brief.314. Remove irrelevant, duplicate, branded-only, and off-intent terms.325. Prioritize by practical opportunity: business fit → clear intent → reasonable KD → volume/CPC signal → winnable SERP.336. `get_serp_results` for high-potential or ambiguous terms when SERP intent would change the call.347. Write the shortlist into `brand/keyword-plan.md` (preserve its required sections per `brand/SCHEMA.md`; confirm before overwriting populated sections).358. Present: best opportunity theme, top keywords now, keywords to save, SERP caveats. Then next actions: `openseo-keyword-clustering`, `seo-content`, or save.3637## Cost Discipline3839- State estimated call counts before bulk pulls (>200 keywords) and get confirmation.40- Small exploratory batches are fine without asking.41- GSC-first ordering exists precisely to avoid spending credits on data the user already owns.4243## Anti-Patterns4445- **Inventing metrics when OpenSEO returns nothing** — because a hallucinated KD of "about 35" silently becomes the foundation of a content plan. If OpenSEO doesn't return a value, write `unknown`.46- **Volume-first prioritization** — because a 10k-volume term that doesn't match the product converts nobody and burns months. Positioning filters the list before metrics rank it.47- **`save_keywords` without explicit confirmation** — because saves mutate the user's OpenSEO account and bulk saves burn credit. Ask, state the count, then save.48- **Skipping the GSC-first pass when GSC is connected** — because striking-distance terms (positions 5–20) are provably the cheapest wins in SEO and they're free to read. Discovery research before first-party data is wasted spend.49- **Overwriting a populated `keyword-plan.md` without confirmation** — because that file is brand memory other skills build on (`seo-content`, `seo-machine`). Merge; confirm destructive rewrites.5051## Close the loop5253After writing files, log completion so `mktg plan` / `mktg status` count the work (bare `mktg run` only logs `loaded`):5455```bash56mktg run openseo-keyword-research --complete --writes <paths written> --result success --json57```5859## Progressive Enhancement6061| Level | Behavior |62|---|---|63| L0 (no OpenSEO) | Hand off to Exa-backed `keyword-research`; metrics `unknown` |64| L1 (`OPENSEO_API_KEY`) | Metrics via available calls; MCP steps deferred |65| L2 (MCP connected) | Full workflow incl. SERP validation |66| L3 (GSC connected) | Striking-distance-first ordering; highest-signal path |6768---6970*Adapted from [every-app/open-seo](https://github.com/every-app/open-seo) `.agents/skills/keyword-research` (MIT). Workflow and tool guidance upstream; mktg brand-memory writes, positioning filter, and cost discipline added here.*