← Files NimbleARCHIVED FILE
skills/seo-intel/references/wf-rank-tracker.md
14.9 KB · Oct 5, 2026 · 18:08 UTC
# SEO Rank Tracker
Live keyword position tracking with JSON snapshots and delta reporting.
---
## Instructions
### Step 0: Preflight
Follow the transport selection + standard preflight from `references/nimble-playbook.md` — pick CLI or MCP at session start, then run the standard preflight calls (date calc, today, profile, memory index) in parallel.
From the results:
- CLI missing or API key unset → `references/profile-and-onboarding.md`, stop
- Tag all `nimble` CLI calls: `nimble --client-source skill-seo-intel <subcommand>`. MCP path: not yet supported — see `references/nimble-playbook.md` for status.
- Profile exists → check for same-day sibling output from `seo-keyword-research`:
```bash
ls ~/.nimble/memory/reports/seo-keyword-research-*$(date +%Y-%m-%d).md 2>/dev/null
```
If found, read the sibling report and extract target keywords from it. These become
the default keyword list for this run — skip keyword questions in Step 2.
Also load any existing snapshot directory for the target domain (determined from the
user's request or business profile):
```bash
ls ~/.nimble/memory/seo/rank-snapshots/ 2>/dev/null
```
This tells us which domains have tracking history.
### Step 1: First-Run Onboarding
Delegate to `references/profile-and-onboarding.md`. If no business profile exists,
run the company setup flow (2 prompts max). Once the profile exists, proceed.
### Step 2: Shape Scope (2 prompts max)
Gather the tracking parameters. If a sibling handoff provided keywords, skip to
the confirmation prompt.
**Prompt 1** — target domain and keywords:
> "Which domain should I track rankings for? And what keywords do you want to
> monitor?"
>
> Provide a list of keywords (one per line or comma-separated), or say
> "use the keywords from my keyword research" if you just ran that.
If the user provides a domain different from their business profile domain, confirm:
"Tracking rankings for **[domain]** — is this a competitor or your own site?"
**Prompt 2** — locale and depth (use AskUserQuestion):
> **Locale and depth:**
> - **Country/locale:** defaults to US/en — change? (e.g., GB/en, DE/de)
> - **Quick check** — positions only, lite search
> - **Full report** — positions + SERP feature analysis on top keywords
Default to US/en and quick check if the user doesn't specify. If < 5 keywords,
always run full report (the cost difference is negligible).
### Step 3: Smart Date Windowing
Scan for the last snapshot:
```bash
ls ~/.nimble/memory/seo/rank-snapshots/{domain}/ 2>/dev/null | sort -r | head -1
```
Classify the run mode based on the most recent snapshot:
| Condition | Mode | Behavior |
|-----------|------|----------|
| No snapshot OR last snapshot > 14 days ago | **Full mode** | Baseline run, no deltas |
| Last snapshot 1-14 days ago | **Delta mode** | Compare current vs previous |
| Last snapshot is today | **Same-day repeat** | Show existing snapshot, ask before re-running |
For same-day repeats: "Already tracked rankings today. Show existing results, or
re-run for fresh positions?"
Inform the user which mode was selected:
- "First rank check for **[domain]** — establishing baseline positions."
- "Last check was **[N days ago]**. Running delta comparison."
### Step 4: WSA Discovery
Discover SERP-related agents in parallel:
```bash
nimble extract:templates list --limit 100 # then filter items for "serp"
nimble extract:templates list --limit 100 # then filter items for "google search"
```
From the results, look for agents that return structured SERP data (organic results,
positions, SERP features). Validate any candidate with:
```bash
nimble extract:templates get --extract-template-name {name}
```
Check that the agent accepts keyword/query input and returns ranked organic results.
If a suitable WSA exists, use it for SERP queries in Step 5 alongside or instead of
`nimble search`. If no suitable WSA is found, use `nimble search` exclusively — do
not fail.
### Step 5: SERP Query Execution
Read `references/serp-query-patterns.md` for query construction rules, SERP feature
extraction, and batching patterns.
**Batch keywords** into groups of ~5 per sub-agent. Spawn up to 4 `nimble-researcher`
agents (`agents/nimble-researcher.md`) with `mode: "bypassPermissions"`.
Each agent receives a prompt like:
```
Check SERP rankings for these keywords against domain "{domain}".
Country: {country_code}, Locale: {locale}
KEYWORDS:
{keyword_list}
RULES:
- Use the **Bash tool** to execute each nimble command.
- Do NOT use run_in_background. All Bash calls must be synchronous.
- Run all searches simultaneously (multiple Bash tool calls in one response).
For each keyword, run:
nimble search --query "{keyword}" --search-depth lite --max-results 20 --country {cc} --locale {locale}
Parse the JSON results. For each keyword:
1. Find the first organic result where the root domain matches "{domain}"
(strip www., trailing slash, protocol when comparing).
2. Record position (1-indexed), ranking URL, title, and description.
3. If the domain does not appear in top 20, record position as null.
Return results as a JSON array:
[
{
"keyword": "...",
"position": 3,
"ranking_url": "https://...",
"title": "...",
"snippet": "...",
"serp_features": ["featured_snippet", "people_also_ask"],
"checked_at": "2026-04-13T..."
}
]
**Note:** `--search-depth lite` returns organic result metadata only — it does NOT
include SERP feature indicators. Set `serp_features` to `[]` for lite queries.
SERP features are detected in the enrichment pass below (full report mode only).
```
**For full report mode — SERP feature enrichment:** After lite queries complete,
identify the top 5 keywords by business priority. Discover a SERP agent at
runtime (do not hardcode template names — see `references/nimble-playbook.md`):
```bash
nimble extract:templates list --limit 100 # then filter items for "google serp"
nimble extract:templates list --limit 100 # then filter items for "search engine"
```
Validate candidates with `nimble extract:templates get --extract-template-name {name}` and cache
the chosen template as `{serp_agent}`. If no suitable SERP agent is found,
skip enrichment and leave `serp_features` as `[]`.
Run the discovered SERP agent for those keywords to get typed SERP entities:
```bash
# {serp_agent} resolved above
nimble extract:templates run --template "{serp_agent}" --params '{"query": "{keyword}", "num_results": 20, "country": "{cc}", "locale": "{locale}"}'
```
The discovered SERP agent returns `data.parsing.entities` — a dict keyed by
entity type. Entity types are **dynamic** — iterate all keys to detect which
SERP features are present. Common mappings to `serp_features`:
`AIOverview` → `"ai_overview"`, `RelatedQuestion` → `"people_also_ask"`,
`Ad` → `"ads"`. For any other entity type key that appears (news, images,
shopping, local, knowledge panels, etc.), add it as a lowercase snake_case
entry in `serp_features`. Do not skip unknown types.
Additional params: `time` (hour/day/week/month/year), `location` (city string
or UULE for geo-specific rankings), `start` (pagination offset: 10=page 2).
See `references/ai-platform-profiles.md` for the full schema.
**Fallback on agent failure:** If any sub-agent returns without results, run those
keyword searches directly from the main context. Do not leave gaps.
### Step 6: Snapshot Storage
Create the snapshot directory if it does not exist:
```bash
mkdir -p ~/.nimble/memory/seo/rank-snapshots/{domain}
```
Write two files simultaneously:
**Snapshot file** — `~/.nimble/memory/seo/rank-snapshots/{domain}/snapshot-{YYYY-MM-DD}.json`:
```json
{
"domain": "example.com",
"date": "2026-04-13",
"country": "US",
"locale": "en",
"keywords_tracked": 15,
"keywords_ranked": 12,
"rankings": [
{
"keyword": "project management software",
"position": 3,
"ranking_url": "https://example.com/features",
"title": "Project Management Features | Example",
"snippet": "...",
"serp_features": ["people_also_ask", "site_links"],
"checked_at": "2026-04-13T14:30:00Z"
}
]
}
```
For keywords where the domain does not appear in top 20, record `"position": null`
and `"ranking_url": null`.
**Canonical keyword list** — `~/.nimble/memory/seo/rank-snapshots/{domain}/keywords.json`:
```json
{
"domain": "example.com",
"updated": "2026-04-13",
"keywords": [
"project management software",
"team collaboration tool",
"task tracking app"
]
}
```
Merge new keywords into the existing list (union, not replace). This file persists
the tracked keyword set across runs so future runs can auto-load it.
### Step 7: Delta Computation
**Skip in full mode** (no previous snapshot) — all positions are new baselines.
**In delta mode**, load the previous snapshot and compute deltas:
```python
delta = previous_position - current_position
```
Positive delta = position improved (moved up). Negative delta = position dropped.
Classify each keyword:
| Condition | Classification |
|-----------|---------------|
| `abs(delta) >= 5` | **Major move** |
| `abs(delta)` is 2-4 | **Notable shift** |
| `abs(delta) <= 1` | **Stable** |
| Previously null, now ranked | **New entry** (entered top 20) |
| Previously ranked, now null | **Drop-out** (fell out of top 20) |
| Both null | **Not ranked** (unchanged) |
Compute summary stats:
- Keywords in positions 1-3, 4-10, 11-20, not ranked — current vs previous
- Total improved, declined, stable
- Average position change
### Step 8: Report Generation
Build the report using the output format below. In delta mode, the TL;DR focuses
on what changed. In full mode, the TL;DR summarizes the current position landscape.
Always include the full keyword rankings table regardless of mode — the TL;DR and
Biggest Movers sections handle the delta focus; the full table is the reference.
### Step 9: Save & Update Memory
Write the report and update memory simultaneously:
- Report → `~/.nimble/memory/reports/seo-rank-tracker-{YYYY-MM-DD}.md`
- Profile → update `last_runs.seo-rank-tracker` in `~/.nimble/business-profile.json`
- Follow `references/memory-and-distribution.md` for wiki updates:
- Update `~/.nimble/memory/index.md` (bump seo/ directory entry)
- Append to `~/.nimble/memory/log.md`:
```markdown
## [YYYY-MM-DD] seo-rank-tracker
- Domain: {domain}, {N} keywords tracked
- Key findings:
- {biggest mover up}
- {biggest mover down or notable pattern}
```
- Add cross-references if the tracked domain matches a known competitor in
`~/.nimble/memory/competitors/`
### Step 10: Share & Distribute
Follow `references/memory-and-distribution.md` for connector detection and sharing.
- **Slack:** TL;DR with biggest movers only — positions and deltas, no full table.
- **Notion:** Full report as a dated subpage.
### Step 11: Follow-ups
Suggest next actions based on findings:
> **Next steps:**
> - Run `seo-keyword-research` to discover new keywords worth tracking
> - Run `seo-site-audit` to fix on-page issues for keywords that dropped
> - Run `seo-content-gap` to find content opportunities around low-ranking keywords
> - Re-run this tracker in a week to measure progress
If keywords dropped significantly, specifically recommend `seo-site-audit` for
those URLs.
---
## Output Format
```markdown
# SEO Rank Tracker: {domain} — {date}
## TL;DR
[3-5 sentences: total keywords tracked, how many ranked, biggest movers,
overall trend direction. In full mode: position distribution summary.
In delta mode: what changed since last check on {previous_date}.]
## Position Summary
| Range | Count | Change vs Last |
|-------|-------|----------------|
| 1-3 (top positions) | {n} | {+/-n or "—" if first run} |
| 4-10 (page 1) | {n} | {"—" if first run} |
| 11-20 (page 2) | {n} | {"—" if first run} |
| Not ranked (>20) | {n} | {"—" if first run} |
**First-run convention:** Use "—" for all delta columns (Change vs Last, Delta).
In the Full Keyword Rankings table, use "BASELINE" instead of "NEW" for first-run
entries. "NEW" is reserved for delta mode when a keyword newly enters the top 20.
**{n} keywords improved, {n} declined, {n} stable.**
## Biggest Movers
### Gains ▲
| Keyword | Previous | Current | Delta | URL |
|---------|----------|---------|-------|-----|
| {keyword} | {pos} | {pos} | +{n} | [link]({url}) |
### Drops ▼
| Keyword | Previous | Current | Delta | URL |
|---------|----------|---------|-------|-----|
| {keyword} | {pos} | {pos} | {-n} | [link]({url}) |
### New Entries
[Keywords that entered the top 20 for the first time.]
### Drop-outs
[Keywords that fell out of the top 20.]
### Top Competitors per Unranked Keyword
For keywords where the target domain is not in the top 20, show the top 3
ranking competitors to give context on who occupies those positions:
| Keyword | #1 | #2 | #3 |
|---------|----|----|-----|
| {keyword} | {domain} | {domain} | {domain} |
This section helps the user understand the competitive landscape for keywords
they don't yet rank for. Skip in delta mode if the unranked set hasn't changed.
## Full Keyword Rankings
| Keyword | Position | Delta | URL | SERP Features |
|---------|----------|-------|-----|---------------|
| {keyword} | {pos} | {delta or "NEW"} | [link]({url}) | {features} |
| {keyword} | — | DROP | — | — |
## SERP Feature Presence
| Feature | Keywords Appearing | Change vs Last |
|---------|-------------------|----------------|
| Featured Snippet | {n} | {+/-n} |
| People Also Ask | {n} | {+/-n} |
| AI Overview | {n} | {+/-n} |
| Image Pack | {n} | {+/-n} |
| Video Pack | {n} | {+/-n} |
| Local Pack | {n} | {+/-n} |
| Knowledge Panel | {n} | {+/-n} |
| Site Links | {n} | {+/-n} |
## What This Means
[2-3 paragraphs: interpret the ranking data. Are positions trending up or down?
Which keyword clusters are strongest? Are SERP features creating opportunities
or squeezing organic clicks? Specific, actionable — not generic SEO advice.]
Close with bulleted recommendations tied to the data, linking to sibling
skills where relevant. Keep these as a final paragraph under What This Means,
not a new top-level section.
```
---
## Error Handling
See `references/nimble-playbook.md` for the standard error table (missing API key,
429, 401, empty results, timeout). Skill-specific errors:
- **Empty SERP result** for a keyword: Retry once with a broader query variant
(e.g., drop quotes, simplify phrasing). If still empty, record position as null
and note "SERP returned no organic results" in the keyword record.
- **Keyword not in top 20:** Record `position: null`, `ranking_url: null`. This is
expected behavior, not an error. Show as "Not ranked" in the report.
- **Rate limit (429):** Reduce sub-agent concurrency from 4 to 2. If still hitting
limits, serialize queries (one at a time). Do not drop keywords.
- **WSA agent error:** Fall back to `nimble search` for that keyword batch. Log which
WSA failed so the user knows.
- **Snapshot parse error:** If a previous snapshot JSON is malformed, treat as first
run (full mode). Warn: "Previous snapshot was corrupted — running fresh baseline."
SHA-256: 9e3d1c31a25de958b8573c43edad96b59245e6bcb82edced0e2d7a3ebf15c650