
Seo Intel
- 60 installs
- 50 repo stars
- Updated July 29, 2026
- nimbleway/agent-skills
Helps with marketing & seo tasks.
About
seo-intel is a Claude Code skill for marketing & seo. It helps solo builders move faster with AI-assisted development.
- seo-intel
- Marketing & SEO
- AI-coding skill
Seo Intel by the numbers
- 60 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,276 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nimbleway/agent-skills --skill seo-intelAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 60 |
|---|---|
| repo stars | ★ 50 |
| Last updated | July 29, 2026 |
| Repository | nimbleway/agent-skills ↗ |
What it does
Helps with marketing & seo tasks.
Files
SEO Intelligence Toolkit
All-in-one SEO intelligence: keyword discovery, rank tracking, technical audits, content gaps, competitor analysis, AI visibility, and GitHub SEO.
User request: $ARGUMENTS
Before running any commands, read references/nimble-playbook.md for Claude Code constraints (no shell state, no &/wait, sub-agent permissions, communication style). Tag all nimble CLI calls: nimble --client-source skill-seo-intel <subcommand>. MCP path: not yet supported — see references/nimble-playbook.md for status.
---
Workflow Router
Detect the user's SEO intent from $ARGUMENTS and route to the appropriate workflow. If the intent is ambiguous or generic ("help me with SEO"), ask which workflow to run.
Available Workflows
| Workflow | Triggers | Reference |
|---|---|---|
| Keyword Research | keyword opportunities, topic clusters, difficulty, what to rank for | references/wf-keyword-research.md |
| Rank Tracker | track rankings, keyword positions, ranking delta, position check | references/wf-rank-tracker.md |
| Site Audit | SEO audit, technical SEO, meta tags, schema, crawl for issues, CWV | references/wf-site-audit.md |
| Content Gap | content gap, keyword gap, compare coverage, missing topics | references/wf-content-gap.md |
| Competitor Keywords | competitor keywords, reverse-engineer SEO, competitor title tags | references/wf-competitor-keywords.md |
| AI Visibility | AI visibility, ChatGPT presence, Perplexity, AI Overview, GEO | references/wf-ai-visibility.md |
| GitHub SEO | github seo, repo discoverability, optimize readme, repo audit | references/wf-github-seo.md |
Intent Detection
Map the request to one workflow:
- Keywords / topics / difficulty / clusters / "what to rank for" → Keyword Research
- Rankings / positions / tracking / delta / monitoring → Rank Tracker
- Audit / crawl / technical / meta / schema / links / CWV → Site Audit
- Gap / missing topics / coverage comparison / "what should I write" → Content Gap
- Competitor site crawl / reverse-engineer / on-page at scale → Competitor Keywords
- AI visibility / ChatGPT / Perplexity / AI Overview / GEO → AI Visibility
- GitHub / repo / README / discoverability → GitHub SEO
If unclear, present the options with AskUserQuestion:
Which SEO workflow should I run?
- Keyword Research — discover keyword opportunities and topic clusters
- Rank Tracker — check and track keyword positions over time
- Site Audit — full technical SEO crawl with JS rendering
- Content Gap — compare content coverage against competitors
- Competitor Keywords — reverse-engineer competitor on-page SEO at scale
- AI Visibility — measure brand presence across 5 AI platforms
- GitHub SEO — audit repository discoverability and README quality
Execution
Once a workflow is identified:
1. Read the corresponding references/wf-{name}.md 2. Follow its instructions from Step 0 (Preflight) through to the output format 3. All references/ paths inside workflow files are relative to this skill's directory
Workflow Chaining
After completing a workflow, suggest natural next steps using sibling workflows. Common chains:
- Site Audit → Keyword Research → Content Gap → Rank Tracker
- Keyword Research → Competitor Keywords → Content Gap
- AI Visibility → Content Gap → Keyword Research
When chaining, read the next workflow file and continue. Context from the previous run (discovered domains, keyword lists, profile data) carries forward — do not re-run preflight or re-ask onboarding questions.
Shared Configuration
All workflows use:
- Business profile + onboarding:
references/profile-and-onboarding.md - Data persistence:
references/memory-and-distribution.md - CLI patterns + constraints:
references/nimble-playbook.md - AI platform agent discovery:
references/ai-platform-profiles.md
AI Platform Profiles
Reference for querying AI platforms and optimizing content for AI visibility.
---
Nimble AI Platform Agent Discovery
Never hardcode agent template names. The catalog changes — new agents appear, old ones get renamed or deprecated. Discover and validate at runtime using the three-layer pattern from nimble-playbook.md.
Layer 1: Category Discovery
Run broad searches to discover all available AI and SERP agents:
nimble agent list --search "ai" --limit 250
nimble agent list --search "chatgpt" --limit 50
nimble agent list --search "perplexity" --limit 50
nimble agent list --search "google ai" --limit 50
nimble agent list --search "gemini" --limit 50
nimble agent list --search "grok" --limit 50
nimble agent list --search "google serp" --limit 100
nimble agent list --search "search engine" --limit 100Layer 2: Session-Specific Narrowing
From the results, identify agents that match the needed surfaces. For AI visibility workflows, look for agents whose description mentions:
- Direct platform querying (ChatGPT, Perplexity, Gemini, Grok, Google AI)
- Structured
answer+sourcesoutput - SERP entity parsing (for Google Search enrichment)
Layer 3: Validation
Validate each candidate before use:
nimble agent get --template-name {discovered-name}Confirm:
- Input param: typically
prompt(conversational) orkeyword/query(search) - Output fields: look for
answer,sources,linksin the schema - Entity structure: SERP agents return
data.parsing.entitiesas a dict keyed
by entity type name (e.g., AIOverview, OrganicResult, RelatedQuestion). Entity types are dynamic — iterate all keys to detect present features.
Cache discovered template names as variables ({chatgpt_agent}, {perplexity_agent}, {serp_agent}, etc.) for the duration of the run.
Execution Patterns
After discovery, use the cached template names:
# AI platform query — substitute discovered name
nimble agent run --agent "{chatgpt_agent}" --params '{"prompt": "...", "skip_sources": false}'
# SERP enrichment — substitute discovered name
nimble agent run --agent "{serp_agent}" --params '{"query": "...", "num_results": 20, "country": "US"}'
# Batch queries (6+ per platform)
nimble agent run-batch \
--shared-inputs "agent: {chatgpt_agent}" \
--input '{"params": {"prompt": "query 1", "skip_sources": false}}' \
--input '{"params": {"prompt": "query 2", "skip_sources": false}}'What to Expect from AI Platform Agents
AI platform agents send a real prompt to the platform and return structured data:
data.parsing.answer— the AI-generated answer textdata.parsing.sources— array of cited sources (URL, title, snippet)data.parsing.links— extracted links from the response
Source arrays vary by platform: some include position fields (startPosition, endPosition) for citation placement; some include source_domain for direct domain matching. Check the validated schema from nimble agent get.
What to Expect from SERP Agents
SERP agents return data.parsing.entities — a dict keyed by entity type name. Each value is an array of records. Common entity types: AIOverview, OrganicResult, RelatedQuestion, RelatedSearch, Ad. Other types may appear (news, images, shopping, local, knowledge panels, featured snippets). Always iterate all keys — do not check only known types.
Common SERP agent params: query, country, locale, location, num_results, start (pagination), time (time range).
When to use SERP agents: Feature enrichment on 3-5 priority keywords per run. When NOT to use: Bulk position checks — use nimble search --search-depth lite for all-keyword sweeps (cheaper, faster).
Agent Tips
- Set
skip_sources: falseon agents that support it to get source citations. - Sources with
startPosition/endPositionshow where in the answer the source
was cited — earlier position = stronger visibility signal.
- Sources with
source_domainsimplify domain matching without URL parsing. - All agent response fields live under
data.parsing.{field}in the JSON. - If an agent fails validation or returns empty, drop that platform for the run
and note reduced coverage. Do not fabricate data for unreachable platforms.
---
Per-Platform Ranking Factor Profiles
Based on Princeton/KDD 2024 GEO study (arXiv:2311.09735), SE Ranking (400K page study), and ZipTie research.
ChatGPT
- Content-answer fit accounts for ~55% of citation likelihood (ZipTie study)
- Domain authority contributes only ~12% — far less than traditional SEO
- Content published within 30 days gets 3.2x more citations than older content
- Preferred formats: direct answers, structured comparisons, statistics-rich content
- Key signal: match ChatGPT's own response style — mirror the concise, list-oriented
format it uses when answering
- Lists and tables are cited more often than prose paragraphs
- Definitions that start with "X is..." pattern get high citation rates
Perplexity
- FAQ schema (JSON-LD) disproportionately rewarded over other structured data types
- Publicly accessible PDFs get priority treatment
- Publishing velocity matters more than keyword targeting — frequent updates win
- Content with explicit source citations is favored (Perplexity prefers citing
content that itself cites sources)
- Position in answer correlates with source quality + freshness
startPosition/endPositionin agent output map to citation placement within
the generated answer — lower startPosition means the source was cited earlier
- Sources appearing in the first paragraph of Perplexity's answer carry the
highest visibility value
Google AI Overviews / Google AI Mode
- E-E-A-T signals dominate: Experience, Expertise, Authoritativeness, Trust
- Structured data (JSON-LD) helps significantly — especially FAQ, HowTo, Product
- Passage-level optimization outperforms page-level (AI extracts passages, not pages)
- Content within Google's knowledge graph entities ranks higher
- Featured snippet winners are 2x more likely to appear in AI Overviews
- Pages ranking in positions 1-5 organically supply ~80% of AI Overview citations
- Long-tail informational queries trigger AI Overviews most frequently
Gemini
- Uses Google's knowledge graph for entity recognition
- Favors authoritative, well-structured content with clear headings
- Source citations often mirror Google AI Overview patterns
source_domainfield in agent output enables direct domain matching- Structured data signals overlap heavily with Google AI — optimize once, benefit twice
Grok
- Integrated with X (Twitter) data — real-time content weighted heavily
- Social signals and trending topics influence responses
- Recency bias is the strongest of any platform
- Less studied than other platforms — treat findings as directional, not definitive
Claude (Reference Only — Not Directly Queryable)
- Uses Brave Search backend (not Google/Bing) for web-grounded responses
- Extremely selective about citations — quality over quantity
- Factual density with specific numbers is the strongest signal
- Crawl-to-refer ratio: 38,065:1 — most crawled pages never get cited
- No Nimble agent available; monitor via robots.txt and Brave Search visibility
- To estimate Claude visibility, check Brave Search rankings for your target queries
and look for ClaudeBot / anthropic-ai access in robots.txt
---
Princeton GEO Optimization Methods
Nine methods ranked by measured visibility boost from the KDD 2024 study (arXiv:2311.09735). Apply these to content blocks targeting AI citation.
| # | Method | Visibility Boost | When to Apply |
|---|---|---|---|
| 1 | Cite Sources | +40% | Add authoritative external citations to claims |
| 2 | Statistics Addition | +37% | Include specific numbers, percentages, data points |
| 3 | Quotation Addition | +30% | Add expert quotes with attribution |
| 4 | Authoritative Tone | +25% | Confident, expert language (not hedging) |
| 5 | Easy-to-Understand | +20% | Simplify complex concepts for broad audience |
| 6 | Technical Terms | +18% | Use domain-specific vocabulary appropriately |
| 7 | Unique Words | +15% | Vocabulary diversity distinguishes from competitors |
| 8 | Fluency Optimization | +15-30% | Readability and natural flow |
| 9 | Keyword Stuffing | -10% | AVOID — actively hurts AI visibility |
Best Combinations
- Fluency + Statistics = highest overall boost across platforms
- Citations + Authoritative Tone = best for professional/B2B content
- Easy Language + Statistics = best for consumer-facing content
Content Block Sizing
Different AI surfaces extract content at different granularities:
- GEO (AI Overview citations): 134-167 word self-contained passage blocks
- AEO (Featured Snippets): 40-55 word direct answer blocks
- Voice search: Under 30 words — one clear sentence
Each block should be self-contained: a reader (or AI) should understand it without needing surrounding paragraphs for context.
Applying GEO Methods to Existing Content
When auditing a page for AI visibility optimization:
1. Identify the target query (what question should this page answer?) 2. Check which AI platforms currently cite the page (run agents above) 3. Score the page against the 9 methods — which are present, which are missing? 4. Prioritize adding the top 3 methods (Citations, Statistics, Quotations) first 5. Restructure content into appropriately-sized blocks for the target surface 6. Re-query platforms after changes to measure improvement
Do not apply all 9 methods to every page. Pick the 3-4 most relevant based on content type and target audience.
---
AI Bot Access
Robots.txt User Agents
Check robots.txt for these crawlers. Blocking them reduces AI visibility.
| User Agent | Platform |
|---|---|
GPTBot | OpenAI training crawler |
ChatGPT-User | ChatGPT browse mode |
ClaudeBot / anthropic-ai | Anthropic's crawler |
PerplexityBot | Perplexity's crawler |
GoogleOther | Google's AI training crawler |
Googlebot | Google's main crawler (also used for AI Overviews) |
Recommendation: Allow all AI bots unless there is a specific, documented reason to block. Each blocked bot is a visibility channel closed.
LLMs.txt
Check for /llms.txt at the site root — the emerging standard for telling AI agents about site structure and capabilities. Presence of this file signals AI-awareness and can improve how AI platforms index the site.
nimble extract --url "https://example.com/llms.txt" --format markdown
nimble extract --url "https://example.com/robots.txt" --format markdownAccess Audit Pattern
For a target domain, run both checks in parallel:
1. Fetch robots.txt and grep for AI bot user agents listed above 2. Fetch /llms.txt and check for structured site description
Report which bots are allowed, which are blocked, and whether llms.txt exists. This is a prerequisite for any AI visibility optimization — if bots cannot crawl, no amount of content optimization will help.
AI Visibility Agent Prompt
Use this template when spawning per-platform nimble-researcher agents in Step 5. Replace all {placeholders} with actual values before passing to the Agent tool.
---
Query AI search surfaces for brand visibility signals on {platform}.
BRAND: {brand}
BRAND DOMAIN: {brand_domain}
SNAPSHOT DATE: {snapshot_date}
COMPETITORS:
{competitors_json}
YOUR QUERY BATCH ({batch_size} queries):
{queries_batch_json}
PLATFORM: {platform}
RULES:
- Use the **Bash tool** to execute each nimble command.
- Do NOT use run_in_background. All Bash calls must be synchronous.
- Do NOT use WebSearch. Only use nimble CLI commands via Bash.
- Speed over depth. Parallel everything. Structured output only.
- No analysis, no interpretation, no file writes.
- Max 4 simultaneous Bash tool calls per response.
EXECUTION — choose based on platform:
If platform = "google_aio":
For each query, make TWO calls:
1. nimble search --query "{query}" --search-depth deep --country US --max-results 10
2. nimble extract --url "https://www.google.com/search?q={url_encoded_query}" --render --driver vx10-pro --format markdown
From call 1: check the structured SERP JSON for AI Overview fields,
organic results, and featured snippets. Note which domains rank.
From call 2: parse the rendered markdown for the AI Overview text block.
Look for a section that contains the synthesized answer (often preceded by
an "AI Overview" heading or similar marker). Extract the full answer text
and any cited source URLs within it.
If platform = "perplexity":
For each query:
nimble extract --url "https://www.perplexity.ai/search?q={url_encoded_query}" --render --driver vx10-pro --format markdown
Parse the extracted markdown for:
- The main answer text (the synthesized response paragraph(s))
- The citations list (numbered source URLs and titles, usually at the
bottom or inline as numbered references)
If platform = "chatgpt_proxy":
For each query:
nimble search --query "{query}" --search-depth deep --include-answer --max-results 20
Parse the JSON response for:
- The "answer" field (LLM-synthesized answer text)
- Source attributions within or alongside the answer
- The organic results list (URLs, titles, domains)
Note: this is a proxy signal for ChatGPT-style sources, not a direct
ChatGPT query. The --include-answer feature provides an LLM answer with
source attribution that approximates ChatGPT's source selection behavior.
DETECTION — apply to every query result regardless of platform:
1. BRAND MENTION: Search the AI answer text for the brand name
"{brand}" (case-insensitive). Also check common variants:
- With/without "Inc", "Corp", "LLC", etc.
- Domain name without TLD (e.g., "acme" for acme.com)
- Known abbreviations
Record: brand_mention = true/false
2. DOMAIN CITATION: Normalize all cited source URLs to root domain
(strip www., protocol, trailing slash, path). Compare against
"{brand_domain}". Record: domain_citation = true/false
3. POSITION: If brand is mentioned, record the character offset of the
first mention in the answer text. If brand domain is cited, record
its ordinal position in the sources list (1-indexed).
4. SENTIMENT: Read the sentence(s) containing the brand mention.
Classify as:
- "positive" — recommends, praises, highlights as a leader
- "neutral" — factual mention without evaluative language
- "negative" — criticizes, warns, notes problems
- "unknown" — mention found but sentiment unclear, or no mention
5. COMPETITOR DETECTION: For each competitor in the competitors list,
apply the same mention + domain citation detection as above.
Record which competitors are mentioned and which domains are cited.
6. ANSWER EXCERPT: Extract 1-2 sentences around the brand mention
(or the most relevant competitor mention if brand is absent).
Keep under 200 characters.
ERROR HANDLING:
- If extraction returns empty or garbage (< 50 chars of meaningful text),
retry once with --driver vx10-pro if not already using it.
- If retry fails, record error and move to next query. Do not abort.
- If nimble search returns 429, pause and retry that single query.
If persistent, record error and continue with remaining queries.
- Never skip a query silently — always return a result object (with
error field populated on failure).
OUTPUT FORMAT:
Return a JSON array. One object per query, in this EXACT structure:
[
{{
"query": "best project management software",
"platform": "{platform}",
"ai_answer_present": true,
"answer_excerpt": "Acme Corp is recommended for teams needing real-time collaboration...",
"brand_mention": true,
"domain_citation": false,
"position_in_answer": 142,
"sentiment": "positive",
"competitor_mentions": ["WidgetCo", "GizmoTech"],
"competitor_domain_citations": ["widgetco.com"],
"sources": [
{{"url": "https://widgetco.com/features", "title": "WidgetCo Features"}},
{{"url": "https://review-site.com/pm-tools", "title": "Best PM Tools 2026"}}
],
"error": null
}},
{{
"query": "acme corp reviews",
"platform": "{platform}",
"ai_answer_present": false,
"answer_excerpt": null, "brand_mention": false,
"domain_citation": false, "position_in_answer": null,
"sentiment": "unknown", "competitor_mentions": [],
"competitor_domain_citations": [], "sources": [],
"error": null
}},
{{
"query": "project management comparison",
"platform": "{platform}",
"ai_answer_present": null,
"answer_excerpt": null, "brand_mention": false,
"domain_citation": false, "position_in_answer": null,
"sentiment": "unknown", "competitor_mentions": [],
"competitor_domain_citations": [], "sources": [],
"error": "Extraction failed: 403 Forbidden after retry"
}}
]
FIELD NOTES:
- "ai_answer_present": true = answer found, false = page loaded but no AI
answer, null = extraction failed
- "position_in_answer": char offset (mention) or ordinal in sources (citation)
- "sources": all cited URLs in the AI answer, not just brand/competitor matches
Do NOT analyze or interpret the data. Return the structured JSON array only.
The parent context handles all scoring and reporting.Audit Extraction Agent Prompt
Use this template when spawning per-batch nimble-researcher agents in Step 7. Replace all {placeholders} with actual values before passing to the Agent tool.
---
Extract SEO data from pages on {domain} for a site audit.
YOUR BATCH ({batch_size} pages):
{page_urls_json}
RENDER TIER: {render_tier}
- Tier 1: nimble extract --url "..." --format markdown
- Tier 2: nimble extract --url "..." --render --format markdown
- Tier 3: nimble extract --url "..." --render --driver vx10-pro --format markdown
Use Tier {render_tier} flags for all extractions.
RULES:
- Use the **Bash tool** to execute each nimble command.
- Do NOT use run_in_background. All Bash calls must be synchronous.
- Do NOT use WebSearch. Only use nimble CLI commands via Bash.
- Speed over depth. Parallel everything. Structured output only.
- No analysis, no interpretation, no file writes.
EXTRACTION STRATEGY:
For batches of 11+ URLs, use extract-batch:
nimble extract-batch \
--shared-inputs 'format: markdown' \
--shared-inputs 'render: {render_flag}' \
--input '{{"url": "https://...page-1"}}' \
--input '{{"url": "https://...page-2"}}' \
...
Where render_flag is:
- Tier 1: omit the render shared-input entirely
- Tier 2: --shared-inputs 'render: true'
- Tier 3: --shared-inputs 'render: true' --shared-inputs 'driver: vx10-pro'
Poll with: nimble batches progress --batch-id <id>
Fetch results with: nimble batches get --batch-id <id>
Then for each task: nimble tasks results --task-id <id>
For batches of 1–10 URLs, make parallel nimble extract calls (max 4 simultaneous
Bash tool calls per response):
nimble extract --url "{url}" {render_flags} --format markdown
STRUCTURED EXTRACTION:
After getting raw markdown for each page, also attempt structured parsing:
nimble extract --url "{url}" {render_flags} --parse --parser '{parser_schema}'
If the parser returns nulls for all fields on a page, fall back to the raw markdown
result and return it with null fields. Do NOT retry — the parent context handles
render tier escalation.
PARSER SCHEMA:
{parser_schema}
OUTPUT FORMAT:
Return a JSON array. One object per page, in this EXACT structure:
[
{{
"url": "https://...",
"status": "success" | "failed" | "parser_empty",
"error": null | "description of error",
"parsed": {{
"title": "..." | null,
"meta_description": "..." | null,
"canonical": "..." | null,
"og_title": "..." | null,
"og_description": "..." | null,
"twitter_card": "..." | null,
"h1": ["..."] | null,
"h2_h6_outline": "..." | null,
"schema_jsonld": ["..."] | null,
"internal_link_count": 0 | null,
"external_link_count": 0 | null,
"img_without_alt_count": 0 | null,
"word_count": 0 | null,
"content_to_html_ratio": 0.0 | null,
"has_hreflang": true | false | null,
"lang_attr": "..." | null,
"status_code": 200 | null,
"canonical_self_referential": true | false | null
}},
"raw_markdown_snippet": "first 500 chars of markdown if parser failed"
}}
]
STATUS VALUES:
- "success": parser returned data for the page
- "parser_empty": parser returned all nulls — raw_markdown_snippet provided instead
- "failed": extraction failed entirely (timeout, 4xx, 5xx) — error field explains why
FALLBACK RULES:
- If extract-batch times out, fall back to individual extract calls for remaining URLs.
- If an individual extract fails, record status "failed" with the error and move on.
- If the parser returns nothing but raw markdown has content, set status "parser_empty"
and include the first 500 characters in raw_markdown_snippet.
- Never abort the batch for a single page failure. Return results for all pages.
Do NOT analyze or interpret the data. Return the structured JSON array only.
The parent context handles all audit logic.Content Gap Extraction Agent Prompt
Use this template when spawning per-domain nimble-researcher agents in Step 4. Replace all {placeholders} with actual values before passing to the Agent tool.
---
Extract and classify content pages from {domain} for a content gap analysis.
YOUR DOMAIN: {domain}
DATE: {date}
FOCUS PREFIX: {focus_prefix}
PAGES TO EXTRACT ({batch_cap} max):
{page_urls_json}
RULES:
- Use the **Bash tool** to execute each nimble command.
- Do NOT use run_in_background. All Bash calls must be synchronous.
- Do NOT use WebSearch. Only use nimble CLI commands via Bash.
- Hard cap on extractions: {batch_cap}. If the page list exceeds this, take
the first {batch_cap} URLs only.
- Speed over depth. Parallel everything. Structured output only.
- No analysis, no interpretation, no file writes.
- Skip failed pages with an error record — never abort the batch.
EXTRACTION STRATEGY:
For 11+ URLs, use extract-batch:
nimble extract-batch \
--shared-inputs 'format: markdown' \
--input '{{"url": "https://...page-1"}}' \
--input '{{"url": "https://...page-2"}}' \
...
Poll with: nimble batches progress --batch-id <id>
Fetch results with: nimble batches get --batch-id <id>
Then for each task: nimble tasks results --task-id <id>
For 1-10 URLs, make parallel nimble extract calls (max 4 simultaneous Bash
tool calls per response):
nimble extract --url "{url}" --format markdown
RENDER ESCALATION:
If a page returns < 100 characters of meaningful content (after stripping
nav/footer boilerplate), retry with --render:
nimble extract --url "{url}" --format markdown --render
If still sparse, escalate to:
nimble extract --url "{url}" --format markdown --render --driver vx10-pro
Only escalate pages that need it — do not re-extract pages that already
returned good content.
PER-PAGE EXTRACTION:
From each page's extracted markdown, determine these fields:
1. url — the page URL
2. title — the <title> tag or first line of content
3. meta_description — parse from `data.html` `<meta name="description" content="...">`.
For content-gap analysis, run an additional `nimble extract --url "{url}" --format html`
call per page (in parallel with the markdown call) to capture meta tags.
meta_description is NOT in markdown output — always parse from raw HTML.
4. h1 — the primary H1 heading
5. h2_outline — list of all H2 headings on the page
6. primary_topic — a single noun phrase that captures the page's main subject.
Derive from the H1 heading. Normalize: lowercase, strip trailing punctuation,
collapse whitespace. Examples:
- "How to Build a Content Marketing Strategy" → "content marketing strategy"
- "The Ultimate Guide to Email Automation" → "email automation"
- "Product Pricing | Acme Corp" → "product pricing"
7. secondary_topics — list of noun phrases from H2 headings, normalized the
same way. Deduplicate against primary_topic.
8. target_keywords — list of 3-5 keywords inferred from the title, H1, and
first paragraph of body content. Include the primary topic as-is, plus
variations and long-tail phrases visible in the content.
9. word_count — approximate word count of the main body content. To exclude
navigation and footer boilerplate: find the H1 heading — body content starts
there. Stop counting at the first occurrence of patterns like "Related Posts",
"Read More", "Subscribe", "Newsletter", "Footer", or a second `---` horizontal
rule. If the H1 is not found, start after the first 500 characters (likely
nav boilerplate) and stop 500 characters before the end (likely footer).
10. content_type — classify as one of:
- blog_post: article with author/date, narrative structure
- product: product or feature page with specs/benefits
- landing: marketing landing page with CTAs
- docs: documentation, help center, or knowledge base
- resource: downloadable resource, template, tool, calculator
- pricing: pricing page with plan tiers
- about: about us, team, careers, contact
- other: anything that doesn't fit the above
11. pub_date — publication or last-modified date if visible on the page
(format: YYYY-MM-DD or YYYY-MM). null if not found.
TOPIC NORMALIZATION RULES:
- Lowercase everything
- Strip leading articles ("the", "a", "an")
- Strip trailing punctuation
- Collapse multiple spaces to single
- Stem plural nouns to singular when unambiguous
(e.g., "strategies" → "strategy", but keep "analytics" as-is)
- Drop brand names from topic labels
(e.g., "Acme Content Marketing" → "content marketing")
RETURN FORMAT:
Return a single JSON object with key "pages" containing an array. Use this
exact structure — no prose, no commentary, no markdown outside the JSON block:
{
"pages": [
{
"url": "https://...",
"title": "...",
"meta_description": "...",
"h1": "...",
"h2_outline": ["...", "..."],
"primary_topic": "...",
"secondary_topics": ["...", "..."],
"target_keywords": ["...", "..."],
"word_count": 1500,
"content_type": "blog_post",
"pub_date": "2025-03-15"
},
{
"url": "https://...",
"error": "extraction failed — 403 Forbidden"
}
]
}
FAILURE HANDLING:
- If extraction fails for a page (timeout, 4xx, 5xx, empty after render
escalation), include it with only "url" and "error" fields.
- Never abort the batch for a single page failure.
- If extract-batch times out, fall back to individual extract calls for
remaining URLs.
- Return results for ALL pages — successes and failures.
Do NOT analyze, interpret, or compare the data across pages. Return the
structured JSON only. The parent context handles all topic modeling and
gap analysis.GitHub SEO Checks
Detailed check rules, severity levels, and scoring logic for the seo-github skill.
---
Scoring Weights
| Category | Weight | Description |
|---|---|---|
| README Quality | 30% | Content, structure, and completeness of the README |
| Metadata | 20% | Repository description, topics, license, homepage |
| Community Health | 20% | Community files, activity signals, responsiveness |
| Search Visibility | 15% | Presence in Google and GitHub search results |
| AI Discoverability | 15% | Mention/citation in AI-generated answers |
Each category produces a score from 0-100. The overall score is the weighted average. Checks within each category are weighted by severity.
Severity weights for check-level scoring:
| Severity | Points if Pass | Points Deducted if Fail |
|---|---|---|
| Critical | 25 | -25 |
| High | 15 | -15 |
| Medium | 10 | -10 |
| Low | 5 | -5 |
Category score = max(0, min(100, base_score + sum(check_adjustments))). Base score starts at 50. Each passing check adds points; each failing check deducts.
---
Category 1: Metadata Checks
| # | Check | Condition | Severity |
|---|---|---|---|
| 1.1 | Description present | description is not null or empty | Critical |
| 1.2 | Description length | 20-200 characters (1-3 sentences ideal) | Medium |
| 1.3 | Description has keywords | Contains at least one domain keyword | Medium |
| 1.4 | Topics present | topics array is not empty | High |
| 1.5 | Topic count | 5-15 topics (fewer = undiscoverable, more = spammy) | Medium |
| 1.6 | Topic coverage | Topics include language, domain, AND use-case terms | High |
| 1.7 | Homepage URL set | homepage is not null when docs/website exists | Medium |
| 1.8 | License present | license field is not null | High |
| 1.9 | OSS-friendly license | MIT, Apache-2.0, BSD-2/3-Clause, ISC, MPL-2.0 | Low |
| 1.10 | Social preview | Custom image set (not GitHub auto-generated) | Low |
| 1.11 | Not archived | archived is false | High |
| 1.12 | Primary language matches topics | Main language appears in topics list | Low |
Recommendations:
- 1.1: Add a description that explains what the project does in one clear sentence.
Include the primary use-case and language/framework.
- 1.2: Keep the description between 20-200 characters. Too short is vague; too long
gets truncated in search results.
- 1.3: Include at least one keyword that users would search for (e.g., "REST API
client" not just "a client library").
- 1.4/1.5: Add 5-15 topics via Settings > Topics. Include the primary language
(e.g., python), the domain (e.g., machine-learning), and use-case terms (e.g., data-pipeline, cli-tool).
- 1.6: Topics should cover three dimensions: language/framework (
typescript,
react), domain (web-scraping, nlp), and use-case (developer-tools, automation). This maximizes discoverability across different search intents.
- 1.7: If the project has a documentation site or homepage, set it in the repo
settings. This links GitHub to external docs and vice versa.
- 1.8/1.9: Add a LICENSE file. MIT and Apache-2.0 are the most adoption-friendly
for open source. Repos without a license deter contributors and corporate users.
- 1.10: Create a custom social preview image (1280x640px). This is what appears
when the repo is shared on social media, Slack, or in link previews. Use the project logo, name, and a one-line description.
---
Category 2: README Checks
| # | Check | Condition | Severity |
|---|---|---|---|
| 2.1 | H1 present | README starts with or contains an H1 heading | Critical |
| 2.2 | H1 matches repo name | H1 text matches or contains the repo name | High |
| 2.3 | Word count adequate | 300-3000 words (< 300 = too thin, > 3000 = bloated) | Medium |
| 2.4 | Code example early | Code block appears within first 500 words | High |
| 2.5 | Installation section | Section headed "Install", "Installation", "Setup", or "Getting Started" | High |
| 2.6 | Usage section | Section headed "Usage", "Quick Start", "Examples", or "Getting Started" | High |
| 2.7 | Badges present | At least one badge image (CI, version, license, downloads) | Medium |
| 2.8 | Multiple badge types | Badges cover 2+ categories (build status, version, license, coverage) | Low |
| 2.9 | Table of contents | TOC present if word count > 500 | Low |
| 2.10 | Contributing info | "Contributing" section or link to CONTRIBUTING.md | Medium |
| 2.11 | License mention | "License" section or mention of license type | Low |
| 2.12 | Documentation links | Links to docs site, wiki, or API reference if they exist | Medium |
| 2.13 | Heading hierarchy | Headings follow H1 > H2 > H3 without skipping levels | Medium |
| 2.14 | No broken links | Sampled links resolve (check 3-5 key links) | Medium |
| 2.15 | Screenshots/demo | Images or GIFs present (for visual/UI projects only) | Medium |
| 2.16 | No stale content | No obviously outdated version numbers, dates, or deprecated badges | Low |
Recommendations:
- 2.1/2.2: Start the README with
# {Repo Name}followed by a one-line description.
This is the first thing users and search engines see.
- 2.3: Aim for 500-1500 words for most projects. Very short READMEs signal abandoned
or trivial projects. Very long READMEs need a table of contents and possibly should move detail into separate docs.
- 2.4: Developers evaluate repos by code first. Show a minimal usage example within
the first screenful. A 3-5 line snippet beats a paragraph of description.
- 2.5: Include a clear installation section with copy-pasteable commands
(npm install, pip install, cargo add, etc.).
- 2.6: Show how to use the project immediately after installation. "Quick Start"
sections with runnable examples reduce time-to-value.
- 2.7/2.8: Badges signal project health at a glance. Prioritize: CI status (green
build), latest version/release, license type, test coverage.
- 2.9: For READMEs over 500 words, add a table of contents. GitHub auto-generates
one in the rendered view, but an explicit TOC in the markdown helps raw readers.
- 2.10: A "Contributing" section or link to CONTRIBUTING.md signals that the project
welcomes contributions. Even "PRs welcome" is better than nothing.
- 2.12: If the project has a docs site, wiki, or API reference, link to it
prominently near the top of the README.
- 2.13: Follow proper heading hierarchy (H1 for title, H2 for sections, H3 for
subsections). Skipping levels (H1 to H3) hurts readability and SEO.
- 2.15: For CLI tools, UI libraries, or visual projects, include a screenshot or
demo GIF. Visual evidence dramatically increases engagement.
---
Category 3: Community Health Checks
| # | Check | Condition | Severity |
|---|---|---|---|
| 3.1 | Code of conduct | CODE_OF_CONDUCT.md or equivalent present | Medium |
| 3.2 | Contributing guide | CONTRIBUTING.md present | Medium |
| 3.3 | Issue templates | .github/ISSUE_TEMPLATE/ directory or config.yml present | Medium |
| 3.4 | PR template | PULL_REQUEST_TEMPLATE.md present | Low |
| 3.5 | Security policy | SECURITY.md present | Medium |
| 3.6 | License file | LICENSE or LICENSE.md present in root | High |
| 3.7 | Recent activity | Last push within 90 days | High |
| 3.8 | Issue responsiveness | Median first response on recent issues < 7 days | Medium |
| 3.9 | Has releases | At least one GitHub Release exists | Medium |
| 3.10 | CHANGELOG present | CHANGELOG.md or equivalent in root | Low |
| 3.11 | Discussions enabled | has_discussions is true (for community projects) | Low |
| 3.12 | Wiki or docs | has_wiki or has_pages is true, or docs link exists | Low |
Recommendations:
- 3.1: Add a CODE_OF_CONDUCT.md. GitHub provides templates (Contributor Covenant is
the most common). This signals a welcoming project.
- 3.2: Add a CONTRIBUTING.md that explains how to set up the dev environment, run
tests, and submit PRs. Reduces friction for new contributors.
- 3.3: Add issue templates for bug reports and feature requests. Templates ensure
reporters provide useful information and reduce triage time.
- 3.5: Add a SECURITY.md with instructions for reporting vulnerabilities. GitHub
surfaces this prominently in the Security tab.
- 3.7: Repos with no activity for 90+ days appear abandoned. If the project is
stable (not abandoned), add a note in the README and make periodic maintenance commits.
- 3.8: Slow issue response drives contributors away. Aim to acknowledge issues
within 48 hours, even if a fix takes longer.
- 3.9: GitHub Releases make the project installable and show up in search results.
Tag releases with semantic versioning.
---
Category 4: Search Visibility Checks
| # | Check | Condition | Severity |
|---|---|---|---|
| 4.1 | Branded search presence | Repo appears in top 10 for "{repo-name} github" | High |
| 4.2 | Site-scoped search | Repo appears in top 10 for "site:github.com {name}" | High |
| 4.3 | Category search | Repo appears in top 20 for "{keyword} {language} library" | Medium |
| 4.4 | Best-of search | Repo appears in top 20 for "best {category} tool" | Medium |
| 4.5 | Topics match queries | GitHub topics overlap with search queries tested | Medium |
Scoring:
- Found in position 1-3: full points
- Found in position 4-10: 70% points
- Found in position 11-20: 40% points
- Not found in top 20: 0 points
Recommendations:
- 4.1/4.2: If the repo does not appear for its own name, the description and README
likely lack the repo name as a keyword. Ensure the repo name appears in the description, H1, and first paragraph of the README.
- 4.3/4.4: Category visibility comes from topics, description keywords, README
content, and external links. Add category-relevant topics and mention the category clearly in the README introduction.
- 4.5: Topics are the primary driver of GitHub Explore and GitHub search ranking.
Align topics with the queries users actually search for.
---
Category 5: AI Discoverability Checks
| # | Check | Condition | Severity |
|---|---|---|---|
| 5.1 | Named mention | Repo is mentioned by name in an AI answer | High |
| 5.2 | URL citation | Repo's GitHub or docs URL appears in AI source citations | High |
| 5.3 | Category recommendation | Repo appears in "best {category}" AI answers | Medium |
| 5.4 | Competitor displacement | Competitors appear in AI answers where repo does not | Medium |
Scoring:
- Mentioned AND cited: full points
- Mentioned but not cited: 60% points
- Not mentioned, but cited in sources: 40% points
- Not mentioned, competitor mentioned instead: 0 points (flag as gap)
- No AI answer triggered: exclude from scoring (neutral)
Recommendations:
- 5.1/5.2: AI models recommend repos they have seen frequently in training data,
Stack Overflow answers, blog posts, and documentation. Increase mentions by: writing blog posts about the project, answering Stack Overflow questions that reference it, getting listed in awesome-lists, and publishing on relevant aggregators (Hacker News, Reddit, Dev.to).
- 5.3: For "best of" queries, ensure the README clearly states the category the
project belongs to and its differentiators. AI models extract this positioning from README content.
- 5.4: If competitors appear but the repo does not, analyze what content those
competitors have that the repo lacks (blog posts, tutorials, Stack Overflow presence, awesome-list inclusions).
---
Grade Thresholds
| Grade | Score | Interpretation |
|---|---|---|
| A | 90-100 | Excellent discoverability. The repo follows all best practices. |
| B | 75-89 | Good. Minor gaps that are easy to fix. |
| C | 60-74 | Fair. Meaningful issues affecting discoverability. |
| D | 40-59 | Poor. Significant gaps limiting visibility and adoption. |
| F | 0-39 | Critical. The repo is nearly invisible in search and AI. |
Keyword Research Agent Prompt
Use this template when spawning per-keyword-cluster nimble-researcher agents in Step 5. Replace all {placeholders} with actual values before passing to the Agent tool.
---
Extract and analyze the top ranking pages for the keyword cluster "{cluster_name}".
ASSIGNED KEYWORDS:
{keywords}
TOP URLS TO EXTRACT:
{top_urls}
RULES:
- Use the **Bash tool** to execute each nimble command.
- Do NOT use run_in_background. All Bash calls must be synchronous.
- Max 10 Bash tool calls total. Prioritize extraction over search.
- For 5+ URLs, use extract-batch instead of individual extract calls.
EXTRACTION:
For each URL above, extract the full rendered page:
nimble extract --url "{url}" --format markdown --render
If extraction fails (empty, garbage, or error), retry once without --render.
If still failing, skip and log the URL. Do not abort the batch.
For 5+ URLs, batch them:
nimble extract-batch \
--shared-inputs 'format: markdown' --shared-inputs 'render: true' \
--input '{"url": "{url1}"}' \
--input '{"url": "{url2}"}' \
...
Poll with nimble batches progress --batch-id {id}, then fetch individual results
with nimble tasks results --task-id {id}.
ANALYSIS PER URL:
From the extracted markdown, determine:
1. Word count (of main content, excluding nav/footer)
2. Heading structure — list all H1, H2, H3 headings
3. Topic coverage — what subtopics does the page cover?
4. Content type — blog post, listicle, comparison, tool page, product page,
landing page, guide, video transcript, other
5. Domain authority signals — brand recognition (Fortune 500? niche startup?),
site age indicators, breadth of content on the topic
6. SERP features — did this URL hold a featured snippet, PAA, or other special
placement? (from the SERP data if available)
RETURN FORMAT:
Return structured findings for each URL. Use this exact format — no prose, no
commentary, no interpretation:
URL: {url}
KEYWORD: {keyword}
DOMAIN: {domain}
CONTENT_TYPE: [blog|listicle|comparison|tool|product|landing|guide|video|other]
WORD_COUNT: [number]
HEADINGS:
- H1: [heading text]
- H2: [heading text]
- H2: [heading text]
- H3: [heading text]
...
TOPICS_COVERED:
- [subtopic 1]
- [subtopic 2]
- [subtopic 3]
...
AUTHORITY_SIGNALS:
- [signal 1 — e.g., "Major brand (HubSpot)", "Niche blog, no brand recognition"]
- [signal 2 — e.g., "Deep topical coverage (50+ pages on topic)"]
...
SERP_FEATURES: [featured_snippet|paa|knowledge_panel|video|none]
EXTRACTION_STATUS: [success|partial|failed]
---
After all URLs are processed, add a cluster summary:
CLUSTER_SUMMARY: {cluster_name}
URLS_EXTRACTED: [N of N]
AVG_WORD_COUNT: [number]
DOMINANT_CONTENT_TYPE: [type]
COMMON_TOPICS: [topics that appear across 2+ pages]
MISSING_TOPICS: [topics covered by only 1 page or none — these are content gaps]
DIFFICULTY_SIGNAL: [Low|Medium|High|Very High — based on authority and depth observed]
---Memory & Distribution
How skills persist knowledge across sessions and distribute reports to external tools.
---
Memory Architecture
All persistence lives under ~/.nimble/ — never touch user project files.
~/.nimble/
├── business-profile.json # Tier 1: Hot cache (see profile-and-onboarding.md)
└── memory/ # Tier 2: Deep storage (loaded on demand)
├── index.md # Global index (one line per directory)
├── log.md # Chronological activity log (append-only)
├── backlog.md # Research questions and knowledge gaps
├── synthesis/ # Cross-entity analysis pages
├── competitors/ # Accumulated intel per competitor
│ └── index.md # Per-directory entity catalog
├── people/ # Contact profiles for meeting prep
│ └── index.md
├── companies/ # Deep-dive research results
│ └── index.md
├── reports/ # Timestamped full skill outputs
├── positioning/ # Per-competitor positioning snapshots
│ └── index.md
└── glossary.md # Industry terms and jargonTier 1 (business-profile.json) — loaded every session. See references/profile-and-onboarding.md for the full schema and update patterns.
Tier 2 (memory/) — loaded on demand when a skill needs deeper context.
Wiki Primitives
The memory directory includes wiki-level files that make the knowledge base navigable, queryable, and self-maintaining:
~/.nimble/memory/
├── index.md # Global summary (one line per directory)
├── log.md # Chronological activity log (append-only)
├── backlog.md # Research questions and knowledge gaps
├── synthesis/ # Cross-entity analysis pages
│ ├── index.md # Per-directory catalog (same format as others)
│ └── competitive-landscape.md # (created dynamically when patterns emerge)
├── competitors/
│ ├── index.md # Per-directory entity catalog
│ ├── widgetco.md
│ └── gizmotech.md
├── people/
│ ├── index.md
│ └── alex-kim.md
├── companies/
│ ├── index.md
│ └── ...
├── reports/
├── positioning/
│ ├── index.md
│ └── ...
└── glossary.mdSkills create index files, log.md, and synthesis/ on first write if missing.
---
Wiki Content Index (Two-Tier)
Indexes live at two levels: a lightweight global index for cross-directory navigation, and per-directory indexes for detailed entity catalogs.
Global Index (~/.nimble/memory/index.md)
One line per directory — entity count and last-updated date. Never lists individual entities. Stays under 30 lines forever.
# Knowledge Index
| Directory | Entities | Last Updated |
|-----------|----------|-------------|
| [[competitors/index]] | 5 | 2026-03-20 |
| [[people/index]] | 3 | 2026-03-15 |
| [[companies/index]] | 8 | 2026-03-18 |
| [[positioning/index]] | 5 | 2026-03-20 |
| [[synthesis/index]] | 2 | 2026-03-20 |Per-Directory Index ({dir}/index.md)
One row per entity file with summary and last-updated date. Owned by the skills that write to that directory. Scales independently — each directory can grow without affecting other indexes.
# Competitors Index
| File | Summary | Updated |
|------|---------|---------|
| [[competitors/widgetco]] | Enterprise SaaS competitor, Series C | 2026-03-20 |
| [[competitors/gizmotech]] | API-first competitor, growing fast | 2026-03-20 |Rules
- Skills read only their directory's index in preflight. competitor-intel reads
competitors/index.md; meeting-prep reads people/index.md. Cross-directory lookups go through the global index first, then the relevant directory index.
- Skills update only their directory's index on write. When a skill creates or
updates an entity file, update the row in that directory's index. Use the entity file's first # Heading as the summary if none exists.
- Global index is updated after directory index changes. Bump the entity count
and last-updated date for the affected directory.
- Created on first skill run if missing. Skills should not fail if an index
doesn't exist — create it with whatever entities are written in that run.
- Obsidian-compatible.
[[path/entity]]links (without.mdextension) work as
wiki links in Obsidian. Path is relative to ~/.nimble/memory/.
---
Chronological Wiki Log (log.md)
~/.nimble/memory/log.md is an append-only timestamped record of skill runs and findings. Grep-friendly format for answering "what did I learn this week?"
# Activity Log
## [2026-03-15] meeting-prep
- Updated: [[people/alex-kim]], [[companies/widgetco]]
- Key findings:
- Alex Kim moved to VP Engineering role
- Interested in API performance benchmarks
## [2026-03-18] company-deep-dive
- Created: [[companies/target-corp]]
- Key findings:
- Series B closed at $30M, Sep 2025
- Expanding into EU market Q2 2026
## [2026-03-20] competitor-intel
- Created: [[competitors/widgetco]], [[competitors/gizmotech]]
- Updated: [[competitors/acme-rival]]
- Key findings:
- WidgetCo launched enterprise tier pricing
- GizmoTech hired new CTO from CloudCorpRules
- Append at the end of the file (oldest first, newest last). Normal writes are
pure appends — no read-insert-rewrite needed. LLMs read the whole file; humans use grep "^## \[" log.md | tail -10 for recent entries.
- Format:
## [YYYY-MM-DD] skill-name— enablesgrep "^## \[" log.md | tail -10. - Content: List entities created/updated (as
[[path/entity]]links), then 2-3
bullet points of key findings. Keep entries concise — this is a log, not a report.
- Rotate entries older than 90 days as a separate maintenance step. After
appending the new entry, check the oldest entries (at the top). If older than 90 days, remove them. This rotation is not part of the normal append — it's a periodic cleanup that triggers during writes. The full reports in reports/ are the permanent record; log.md is for recent activity scanning.
- Created on first skill run if missing.
---
Cross-Entity References
Entity files use Obsidian-compatible [[path/entity]] wiki links to connect related entities across directories.
Format
# Alex Kim
## Current Role
VP of Engineering at [[competitors/widgetco]] (since 2024)
## Related
- Employer: [[competitors/widgetco]]
- Previous: [[companies/cloudcorp]]# WidgetCo
## Key People
- [[people/alex-kim]] — VP Engineering
- [[people/jane-smith]] — CEO
## Related Competitors
- [[competitors/gizmotech]] — overlapping market segmentRules
- Link format:
[[directory/entity-slug]]— no.mdextension, path relative
to ~/.nimble/memory/. Obsidian resolves these as wiki links.
- Add cross-references when relationships are discovered. When a skill finds
that a person works at a tracked company, or two competitors share a market segment, add links in both directions.
- Handle missing targets gracefully. A cross-reference to a file that doesn't
exist yet is fine — it becomes a valid link once that entity is created. Skills should not fail on dangling links.
- Skills follow links to enrich output. When meeting-prep finds
[[competitors/widgetco]] in a person's file, it loads that competitor file for additional context. When competitor-intel finds [[people/alex-kim]] in a competitor file, it can surface that relationship in the briefing.
When to Add Cross-References
| Relationship discovered | Link from | Link to |
|---|---|---|
| Person works at company | people/{name} → competitors/{company} or companies/{company} | Reverse link too |
| Companies compete | competitors/{a} → competitors/{b} | Reverse link too |
| Person previously at company | people/{name} → companies/{company} | — (one-way is fine) |
| Synthesis cites entity | synthesis/{topic} → entity files | — (one-way) |
---
Ad-Hoc Insights
When a user signals "save this", "remember that", "note this down", or similar intent during a conversation, file the insight into the relevant entity file(s) instead of letting it vanish into chat history.
Filing Pattern
1. Identify the relevant entity file(s). If the insight is about a competitor, file it in competitors/{name}.md. If it spans multiple entities (e.g., "WidgetCo is partnering with GizmoTech"), update all relevant files. 2. Append under a dated `## Insights` section:
## Insights
### 2026-03-22
- User noted: WidgetCo's enterprise pricing is 2x ours — [[competitors/gizmotech]]
is closer to our price point [ad-hoc]3. Add cross-references if the insight connects entities (as shown above). 4. Update the directory's `index.md` — bump the last-updated date for the affected file(s), and update the global index.md counts. 5. Append to `log.md` (at the end of the file):
## [2026-03-22] ad-hoc-insight
- Updated: [[competitors/widgetco]], [[competitors/gizmotech]]
- Key findings:
- WidgetCo enterprise pricing is 2x user's, GizmoTech closer to parityRules
- Tag with `[ad-hoc]` so skills can distinguish user-contributed insights from
skill-generated findings during dedup.
- Multi-entity insights update all relevant files with cross-references between
them.
- Don't create entity files for throwaway comments. If the user says "remember
that meetings on Fridays are bad", that's a preference (update business-profile.json), not an entity insight.
---
Cross-Entity Synthesis Pages
~/.nimble/memory/synthesis/ contains pages that analyze patterns across multiple entity files. Unlike entity files (which accumulate facts about one entity), synthesis pages draw conclusions across the knowledge base.
Page Creation
Synthesis pages are created dynamically when patterns emerge across entities — not from a pre-defined list. Common examples:
| Page | Purpose | Typical trigger |
|---|---|---|
competitive-landscape.md | Market positioning, feature gaps, pricing comparison | competitor-intel after 3+ competitors |
pricing-trends.md | Pricing pattern analysis across competitors | Pricing signals recur across 3+ competitor runs |
Page names should be slug-formatted topic labels (not skill names). Skills create synthesis pages when a pattern recurs across 3+ entities — this keeps synthesis data-driven rather than speculative.
Format
Synthesis pages use YAML frontmatter to track which entity files they were built from and when. This makes staleness deterministic — compare current file timestamps against the recorded ones.
---
confidence: high
sources:
- path: competitors/widgetco.md
updated: 2026-03-20
- path: competitors/gizmotech.md
updated: 2026-03-20
- path: competitors/acme-rival.md
updated: 2026-03-18
generated_by: competitor-intel
generated_at: 2026-03-20
---
# Competitive Landscape
## Market Map
[Positioning of each competitor by segment, size, strategy]
## Feature Comparison
| Capability | Us | [[competitors/widgetco]] | [[competitors/gizmotech]] |
|---|---|---|---|
| Real-time data | ✅ | ❌ | Partial |
## Pricing Comparison
[Tier-by-tier comparison where known]
## Key Patterns
- Trend 1 with evidence from multiple competitors
- Trend 2 with cross-entity citations
## What This Means
[Strategic implications — what the patterns suggest for the user's company]Rules
- Cite source entity files with
[[path/entity]]links. Every claim must trace
back to an entity file.
- Track sources and confidence in frontmatter.
confidence: high|medium|low
reflects data completeness (high = all key sources available, low = sparse data). The sources: block lists every entity file used and its last-modified date at generation time. To check staleness, compare current file timestamps against the recorded ones — if any source was updated since generation, the page is stale.
- Refresh when sources are stale. If a skill adds major new signals to 2+ source
entities since the synthesis was generated, regenerate. Don't regenerate on every run — only when the source timestamps diverge.
- Use `nimble-analyst` agent for synthesis generation. The analyst has the right
model (Sonnet) for cross-entity pattern recognition and strategic analysis.
Generation Trigger
competitor-intel generates competitive-landscape.md when:
- 3+ competitors have been researched in the current run, OR
- The existing synthesis page's source timestamps are stale (source entities were
updated since generation)
Other synthesis pages are created by the relevant skills when patterns emerge, or on user request.
---
Research Backlog (backlog.md)
~/.nimble/memory/backlog.md tracks knowledge gaps and research questions — things to investigate in future skill runs. This is not synthesis (derived, read-only output) — it's imperative (drives future action).
# Research Backlog
## Open
- [ ] WidgetCo pricing for enterprise tier — couldn't find public pricing [2026-03-20, competitor-intel]
- [ ] GizmoTech Series B details — rumored but unconfirmed [2026-03-20, competitor-intel]
- [ ] Alex Kim's LinkedIn activity — profile was private [2026-03-15, meeting-prep]
## Resolved
- [x] WidgetCo new CTO name — confirmed: Sarah Chen [2026-03-22, competitor-intel]Rules
- Any skill can append questions to the
## Opensection when it encounters
gaps during research. Tag each with date and skill name.
- Users can add questions via ad-hoc insights ("find out about X next time").
- Skills check backlog before running to avoid re-researching resolved questions
and to prioritize open ones relevant to the current run.
- Resolved questions get moved to
## Resolvedwith a resolution date — not
deleted. This preserves the audit trail.
---
Deep Storage Formats
competitors/
One file per competitor. Append new findings under dated headers — never overwrite.
# WidgetCo
## Key Facts
- Domain: widgetco.com
- HQ: San Francisco
- Funding: Series C ($45M, Jan 2026)
- CEO: Jane Smith
## Signals
### 2026-03-20
- Launched new enterprise tier pricing — [source URL]
- Hired VP of Sales from CRMHub — [source URL]
### 2026-03-13
- Announced partnership with AWS — [source URL]people/
One file per contact. Used by meeting-prep skill.
# Alex Kim
## Current Role
VP of Engineering at WidgetCo (since 2024)
## Background
- Previously: Senior Director at CloudCorp (2019-2024)
- Education: MS Computer Science, top-10 program
## Notes from Previous Meetings
### 2026-03-15
- Interested in our API performance benchmarks
- Prefers technical depth over high-level summariescompanies/
Detailed company profiles from deep-dive research.
# Target Corp
## Overview
- Industry: Enterprise SaaS | Founded: 2015 | HQ: Austin, TX | ~500 employees
## Financials
- Last funding: Series B ($30M, Sep 2025) | Revenue: Est. $40M ARR
## Recent News
(dated entries, same format as competitors/)reports/
Timestamped full skill outputs. Save the complete briefing, not a summary.
Naming: {skill-name}-{YYYY-MM-DD}.md — if a skill may produce multiple reports per day (e.g., meeting-prep for different companies), add a qualifier: {skill-name}-{qualifier}-{YYYY-MM-DD}.md. The qualifier is defined in each skill's SKILL.md (e.g., company slug for meeting-prep).
glossary.md
Industry terms and jargon. Updated when the user uses unfamiliar terms.
Bootstrapping (First Run)
mkdir -p ~/.nimble/memory/{competitors,people,companies,reports,positioning,synthesis}Create stub files for each competitor from the onboarding flow.
index.md and log.md are created automatically on the first skill run that writes to memory — no need to create empty stubs during bootstrapping.
Differential Analysis
The key feature across all skills — only surface what's genuinely new.
Dedup Lifecycle
Memory loading happens at two points in every skill:
1. Step 0 (Preflight): Load relevant memory files for context. This tells the skill what's already known so it can pass known signals to sub-agents for dedup during research. For example, competitor-intel loads ~/.nimble/memory/competitors/*.md; meeting-prep loads ~/.nimble/memory/people/*.md.
2. Analysis step (before report generation): Final dedup check. Compare all findings from research against loaded memory. Only signals classified as NEW or UPDATED (per the freshness classification in nimble-playbook.md) make it into the report.
What "new" means
- "WidgetCo raised a Series C" is noise if already in memory
- "WidgetCo just hired a new CTO" is a new signal worth highlighting
- "WidgetCo raised a Series C" with a new detail (amount, lead investor) is an UPDATE
Learning from Corrections
When the user corrects the skill, update both tiers:
| Correction | Profile update | Deep storage update |
|---|---|---|
| "Skip CompanyX" | preferences.skip_competitors | Archive file |
| "Track CompanyY" | competitors list | Create stub file |
| "That info is wrong" | — | Update the file |
| "ARR means Annual Recurring Revenue" | — | Add to glossary.md |
| "I prefer bullet points" | preferences.output_format | — |
Always confirm the update to the user.
Checkpointing & Resume
For multi-phase pipelines (map → extract → enrich → score), save intermediate results so failed or interrupted runs can resume without re-doing completed work.
Storage
~/.nimble/memory/{skill-name}/checkpoints/{slug}/
├── map.json # Phase 1 output
├── extract.json # Phase 2 output
└── enrich.json # Phase 3 output{slug} is a stable identifier derived from the run's input parameters (e.g., URL domain, search query hash). Same input = same slug = resumable.
Checkpoint format
Each phase file is JSON:
{
"phase": "extract",
"status": "complete",
"timestamp": "2026-04-03T15:30:00Z",
"record_count": 47,
"data": [ ... ]
}status is "complete" or "partial" (interrupted mid-phase).
Resume logic
On re-run with the same parameters: 1. Detect existing checkpoint directory for the slug 2. Offer: "Found previous run (47 records from Apr 3). Resume and fill gaps, or start fresh?" 3. If resume: skip phases where status = "complete", re-run where status = "partial" or file is missing 4. If start fresh: delete the checkpoint directory and begin from phase 1
Rules
- One checkpoint directory per unique run (keyed by slug)
- Clean up checkpoints older than 30 days on skill startup
- Don't checkpoint trivial runs (< 5 records) — the overhead isn't worth it
Rules
- Never touch user project files. All persistence under
~/.nimble/. - Append, don't overwrite. Deep storage grows over time with dated sections.
- Read on demand. Only load files when the skill actually needs them.
- Update profile after every run. At minimum,
last_runstimestamp. - Update wiki files after every memory write. Update the directory's
index.md
for affected entities, bump the global index.md counts, append a log.md entry for the run, and add cross-references where relationships are discovered.
- Handle missing gracefully. If a file doesn't exist, create it. This includes
index files, log.md, backlog.md, and cross-reference targets.
---
Source Links Enforcement
Every signal in every report must include a clickable source URL. This is a hard requirement — reports without source links are incomplete and must not be distributed.
What counts as a source link:
- A direct URL to the article, press release, or page where the signal was found
- The URL returned by
nimble searchin the result'surlfield - For extracted content, the URL passed to
nimble extract --url
What does NOT count:
- A company's homepage (unless the signal is specifically about homepage content)
- A generic domain without a path (e.g.,
https://widgetco.com) - "Source: web search" or any non-clickable attribution
If a signal has no source URL after research and extraction, drop it from the report. An unsourced signal is worse than a missing one — it can't be verified and erodes trust.
---
Report Distribution
After presenting output, offer sharing based on available MCP connectors.
Connector Detection
Check before presenting options:
- Notion:
mcp__plugin_Notion_notion__notion-create-pages - Slack: Any Slack MCP tool
Sharing Flow
Use AskUserQuestion with only the available options:
Share this report?
- Save to Notion — full report as a page
- Send to Slack — TL;DR to a channel
- Both
- Skip
Notion: Create a dated subpage. If integrations.notion.reports_page_id exists in the profile, use it as parent. Otherwise ask and save the ID for next time.
Slack: Post TL;DR only — Slack is for alerts, not full reports. If integrations.slack.channel exists, use it. Otherwise ask and save.
Neither available (first run only):
Tip: If you connect a Notion or Slack MCP server, I can save reports or post
TL;DRs to your team automatically.
Don't repeat this tip on subsequent runs.
Nimble Playbook
How to run Nimble CLI commands in Claude Code. Read this before executing any commands.
---
Claude Code Execution Rules
- No shell state persistence. Variables set in one Bash call are gone in the next.
Inline all values (dates, paths, names) directly into every command.
- No `&` + `wait` parallelism. It breaks in Claude Code. Instead, make **multiple
Bash tool calls in a single response** — they run in parallel natively.
- Search returns JSON —
--output-formatdoesn't change this. With `--search-depth
lite`, the JSON is small (title, description, URL per result). Parse it directly.
- Extract returns JSON with `data.markdown` — use
--format markdownto get clean
content in the data.markdown field.
Preflight Pattern
Transport selection (run once per session)
Skills work via two transports — CLI (preferred, full surface area) or MCP (fallback, curated tool set covering the same operations). Pick one at the start of every session and stick with it; don't re-probe on every command.
| Check | If it works | What to use |
|---|---|---|
nimble --version (>= 0.12.0) and NIMBLE_API_KEY is set | CLI is ready | Bash nimble ... commands |
| `claude mcp list 2>/dev/null \ | grep -q "nimble" (or first mcp__plugin_nimble_nimble__*` call succeeds) | Plugin MCP is connected |
mcp__plugin_nimble_nimble__* tools are listed, but a read-only nimble_agents_list probe returns an auth / not-connected error or an OAuth authorization URL | Plugin is installed but the connector isn't connected (typical Cowork / claude.ai state) | Stop — guide connector connection (below). Never invent an auth-completion flow. |
| None of the above | Stop — guide install (below) | — |
Connector not connected (Cowork / claude.ai) — verify BEFORE working
In Cowork / claude.ai the plugin is often installed while its connector is not yet connected, so live data calls fail. Confirming the connection is a required preflight step — not an error to react to mid-task. When mcp__plugin_nimble_nimble__* tools are listed but you haven't confirmed the connector is live, run one read-only probe before any real work:
- A single
nimble_agents_listcall is the cheapest confirmation. Success →
connected, proceed. Auth / not-connected error, or a response containing an OAuth authorization URL → not connected.
When not connected, surface this verbatim and stop — do not fall back to WebFetch, WebSearch, curl, or any other tool, and do not guess at data:
Your Nimble plugin is installed, but its connector isn't connected yet — that's
why I can't fetch live data. To connect it:
>
1. Open Customize → Connectors
2. Find Nimble and click Connect
3. Complete the login in your browser. No Nimble account? You can create one
right there during login.
4. Once it shows Connected, re-run your request and I'll continue.
If a tool hands back an OAuth "Authorize" URL
A not-connected tool call may return an authorization link (e.g. "Authorize Nimble MCP →") instead of data. Present that link to the user exactly as given, then stop and wait. Hard rules:
- Never invent a completion flow. There is no "paste the URL from your address
bar back to me" step, and you cannot "complete the connection" yourself. Claiming either is a hallucination.
- Never say the tools "will activate" and then call them in the same turn. Wait
for the user to confirm they've authorized, then retry.
- To check whether authorization succeeded, run one read-only
nimble_agents_list
probe — don't assume.
No plugin and no CLI
If neither path works at all (no plugin installed, no CLI installed), surface this hint verbatim and stop:
Nimble isn't installed. Pick the path for your environment:
>
Any Claude product (Claude Code, Claude Cowork, claude.ai) — recommended:
```
/plugin install nimble
```
Installs the Nimble plugin. The.mcp.jsoninside the plugin auto-registers as a Connector inCustomize → Connectors. First tool call triggers the OAuth flow — no API key needed.
>
Codex CLI or other terminal agents (shell access, no `/plugin`):
```
npm i -g @nimble-way/nimble-cli
```
Thenexport NIMBLE_API_KEY=<key>and re-run. Seereferences/profile-and-onboarding.mdfor the full install flow.
>
Cursor, VS Code, or any other MCP client:
Paste this into your MCP settings (.cursor/mcp.json or host equivalent):```json
{
"mcpServers": {
"nimble": { "type": "http", "url": "https://mcp.nimbleway.com/mcp" }
}
}
```
The plugin path (/plugin install nimble) is the easiest onboarding everywhere it works — one command, OAuth handles auth, no API key to manage. Use the CLI path only when shell access is available but /plugin install isn't (Codex, raw terminal agents). Use the manual mcp.json path only for MCP clients outside the Claude family.
Standard preflight (run in parallel after transport is selected)
Every skill kicks off with these simultaneous calls:
python3 -c "from datetime import datetime, timedelta; print((datetime.now() - timedelta(days=14)).strftime('%Y-%m-%d'))"(14 days ago)date +%Y-%m-%d(today)cat ~/.nimble/business-profile.json 2>/dev/null(profile — fall back to MCP filesystem tool if shell unavailable)cat ~/.nimble/memory/index.md 2>/dev/null(global wiki index — know what directories have data)
Don't skip the transport check — running CLI commands when only MCP is available (or vice versa) wastes a turn and confuses the user.
Request Attribution
All Nimble API calls must carry a client_source tag so usage can be tracked per skill. The value is always skill- followed by the exact SKILL.md name field (e.g. skill-competitor-intel, skill-seo-intel, skill-nimble-web-expert).
CLI path — add --client-source skill-{name} as the global flag on every nimble command. Place it immediately after nimble, before the subcommand. No shell state persistence means this must be inlined on every individual call:
nimble --client-source skill-{name} search --query "..."
nimble --client-source skill-{name} extract --url "..."
nimble --client-source skill-{name} agent run --agent <name> --params '{...}'
nimble --client-source skill-{name} map --url "..."
nimble --client-source skill-{name} crawl run --url "..."MCP path — per-skill client source tracking is not yet supported by the MCP server (it currently sends X-Client-Source: nimble_mcp_server for all calls regardless of skill). This will be enabled once the MCP server adds CLIENT_SOURCE support — no action needed here until then.
Sibling Handoff
When skills in the same family chain together (e.g., extract → enrich → verify), the second skill can skip redundant preflight work. Detect a sibling handoff by checking for same-day output from the upstream skill:
ls ~/.nimble/memory/reports/{upstream-skill}-*$(date +%Y-%m-%d).md 2>/dev/nullUse the dated report as the recency signal — data files under memory/{skill}/ may not have dates in their filenames, so always verify via the report timestamp. If a same-day report exists, parse the slug from the filename and load the corresponding data files.
If same-day sibling output exists:
- Skip CLI check and profile load — they were validated minutes ago
- Reuse WSA Layer 1 and Layer 3 inventory — the catalog hasn't changed. Only
re-run Layer 2 if the specialty or context changed.
- Use the sibling's structured output directly — if the upstream skill produced
data files with domains and page URLs, don't re-search for what's already known. Construct URLs from known patterns instead of running N web searches.
If no same-day sibling output exists: Run full preflight as normal.
This pattern is optional — skills MUST still work standalone without sibling output. The handoff is a fast path, not a requirement.
Smart Date Windowing
For any skill using --start-date based on previous runs:
- First run: 14 days ago → full mode
- Last run < 3 days ago: use 7 days ago (too narrow = empty results) → quick refresh
- Last run 3-14 days ago: use the last run date → quick refresh
- Last run > 14 days ago: 14 days ago → full mode
- Same-day repeat: if
last_runs.{skill-name}is today, check if a report already
exists at ~/.nimble/memory/reports/{skill-name}*[today].md. If it does, ask the user before re-running: "Already ran today. Run again for fresh data?" Don't silently re-run — it wastes API credits and produces near-identical output. Exception — meeting-prep: Skip the same-day report check. Meeting-prep is per-meeting, not per-day — users may prep for multiple meetings in a single day. Instead, meeting-prep checks freshness at the entity level: load cached profiles from ~/.nimble/memory/people/ and ~/.nimble/memory/companies/ and offer to reuse recent research rather than blocking the run.
---
Search
# Standard search (always use --search-depth lite for discovery)
nimble search --query "company name news" --max-results 10 --search-depth lite
# News-focused search
nimble search --query "company name" --focus news --max-results 10 --search-depth lite
# Date-filtered search (inline the date — don't use variables)
nimble search --query "company funding" --focus news --start-date "2026-03-11" --max-results 10 --search-depth lite
# Social signals from X/LinkedIn
nimble search --query "Company" --include-domain '["x.com", "linkedin.com"]' --max-results 10 --search-depth lite --time-range week
# Deep search (full page content — only for comprehensive analysis, costs more)
nimble search --query "company name" --search-depth deep --max-results 5
# Fast search (premium tier — not used by default)
# nimble search --query "company name" --search-depth fast --max-results 10Key flags:
--query— search query string (required)--focus—general,news,shopping,social,coding,academic.
`social` searches social platform people indices directly (LinkedIn, X) — best for finding specific people. If it errors, use --include-domain '["linkedin.com"]' as an alternative approach.
--max-results— max results to return--start-date/--end-date— date filters (YYYY-MM-DD)--search-depth—lite(1 credit),deep(1 + 1/page)--include-domain— JSON array of domains, e.g.,'["x.com", "linkedin.com"]'--time-range— e.g.,week--country— geo-targeted results (e.g., "US", "IL")--include-answer— LLM-powered answer summary
Date range strategy:
- First run: 14 days ago
- Subsequent runs:
last_runstimestamp from business profile - If < 3 results: retry without
--start-date
Extract
# Extract article content as markdown (default for content analysis)
nimble extract --url "https://example.com/article" --format markdown
# Extract raw HTML (required for <head> metadata: canonical, schema, og, meta tags)
nimble extract --url "https://example.com" --format html
# Extract with JavaScript rendering (for dynamic/SPA pages)
nimble extract --url "https://example.com/spa" --render --format markdownResponse is JSON. The field returned depends on --format:
--format markdown→data.markdown(clean body content)--format html→data.html(raw HTML including<head>)--format plain_text→data.plain_text--format simplified_html→data.simplified_html
Format selection by use case:
| Need | Format | Why |
|---|---|---|
| Article body content, word count, headings | markdown | Clean text, no nav/footer noise |
| Meta tags (title, description, canonical, og, twitter) | html | Markdown strips <head> |
| Schema markup (JSON-LD) | html | Script tags not in markdown |
hreflang, <html lang> | html | Attributes not in markdown |
| Structured field extraction | --parse --parser '{...}' | LLM extracts specific fields |
| Both body and head | markdown + html | Two calls or parse html for both |
Key flags:
--url— target URL (required)--format—markdown,html,simplified_html,plain_text(pick based on table above)--render— render JavaScript using a browser--parse --parser '{...}'— structured extraction via LLM parser schema
Extraction fallback (if data.markdown is mostly JavaScript/boilerplate): 1. Garbage check: If data.markdown has < 100 characters of meaningful content (after stripping nav/footer boilerplate), treat it as garbage. 2. Retry with --render --format markdown (handles JS-heavy/SPA pages) 3. If still garbage: search for the same article title on a different domain 4. If still nothing: skip and log — never abort a batch for a single extraction failure
Extract async & batch
# Async — submit single URL, get task_id, poll for results
nimble extract-async --url "https://example.com/page" --render --format markdown
# Batch — up to 1,000 URLs in one request
nimble extract-batch \
--shared-inputs 'render: true' --shared-inputs 'format: markdown' \
--input '{"url": "https://example.com/page-1"}' \
--input '{"url": "https://example.com/page-2"}'Poll async tasks with nimble tasks get --task-id <id> and fetch results with nimble tasks results --task-id <id>. Poll batches with nimble batches progress --batch-id <id>.
Map & Site Mapping
nimble map --url "https://example.com/blog" --limit 20Site Mapping Pattern
Use nimble map to discover a site's page structure, then score and filter pages by relevance before extracting.
1. Discover: nimble map --url {url} --limit {cap} — returns a list of URLs 2. Score: Each skill defines a keyword/weight table for URL path segments (e.g., /providers = High, /about = Medium, /blog = Low). Score each discovered page against the table. 3. Filter: Keep pages scoring above the skill's threshold. Always include the homepage as a fallback. 4. Fallback: If nimble map returns < 3 candidates, use nimble search --query "site:{domain} {keywords}" --max-results 10 --search-depth lite
Each skill provides its own keyword/weight table in SKILL.md — the pattern here is the discover → score → filter → fallback flow.
Agents
Pre-built extraction templates for structured data from specific sites (Amazon, LinkedIn, Google, etc.). Use when you need structured fields rather than raw page content.
# List available agents (search by domain or vertical)
nimble agent list --limit 100
nimble agent list --limit 100 --search "amazon"
# Inspect an agent's schema (input params + output fields)
nimble agent get --template-name <agent_name>
# Run an agent (sync — waits for result)
nimble agent run --agent <agent_name> --params '{"key": "value"}'
# Run an agent (async — returns task_id, poll for results)
nimble agent run-async --agent <agent_name> --params '{"key": "value"}' \
--callback-url "https://your.server/callback"Key flags for `run` / `run-async`:
--agent— agent name fromnimble agent list(required)--params— JSON object with agent input parameters (required)--localization— enable zip_code/store_id localization (agent-dependent)
Additional flags for `run-async`:
--callback-url— POST callback when task completes--storage-type—s3orgs--storage-url— destination bucket URL--storage-compress— gzip the stored output--storage-object-name— custom filename instead of task_id
Response: data.parsing contains the structured output. Shape depends on agent type:
- PDP (product/profile/detail) → flat dict
- SERP / list → array of objects
- Google Search →
{"entities": {"OrganicResult": [...], ...}}
Async task states: pending → success or error. Poll with nimble tasks results --task-id <task_id>.
Fallback rule: If no agent exists for the target domain, fall back to nimble search + nimble extract. Don't fail silently — log which domains lacked agent coverage so agent-builder can fill gaps later.
Agent batch
# Up to 1,000 agent requests in one call
nimble agent run-batch \
--shared-inputs 'agent: amazon_serp' \
--input '{"params": {"keyword": "iphone 15"}}' \
--input '{"params": {"keyword": "iphone 16"}}'Returns a batch_id. Poll with nimble batches progress --batch-id <id>, then fetch individual results with nimble tasks results --task-id <id>.
Tasks & batches polling
# Single async task
nimble tasks get --task-id <task_id> # check status
nimble tasks results --task-id <task_id> # fetch results
# Batch
nimble batches progress --batch-id <batch_id> # lightweight progress check
nimble batches get --batch-id <batch_id> # all task IDs + states
nimble batches list --limit 20 # list all batches
nimble tasks list --limit 20 # list all tasksWorkflow: Always nimble agent get before nimble agent run to understand the expected input params and output fields.
Agent Creation (generate → poll → iterate → publish)
Create custom extraction agents for any website. The full lifecycle is available via CLI.
# Generate a new agent
nimble agent generate \
--agent-name niche_store_pdp \
--prompt "Extract product name, price, rating, and first 5 reviews" \
--url "https://example.com/products/widget-pro"
# Refine an existing agent (clone + apply new prompt)
nimble agent generate \
--agent-name niche_store_pdp \
--from-agent niche_store_pdp \
--prompt "Add a discount_percentage field"
# Poll generation status (async — typically 1-3 min)
nimble agent get-generation --generation-id <generation_id>
# Publish when satisfied
nimble agent publish --agent-name niche_store_pdp --version-id <version_id>Key flags for `generate`:
--agent-name— name for the agent (required)--prompt— natural language description of what to extract (required)--url— sample URL to analyze (required)--from-agent— existing agent to clone and refine (for iteration)--input-schema— custom input schema (optional, inferred if omitted)--output-schema— custom output schema (optional, inferred if omitted)--metadata— additional metadata (optional)
Generation response: returns id (generation ID), status (queued → in_progress → success / failed), and generated_version_id on success.
Workflow: Generate → poll with get-generation until success → optionally iterate with --from-agent → publish with version-id.
Polling: Generation takes 1-3 minutes. Run the generate → poll → publish loop as a background Task agent so the user isn't blocked waiting. The Task agent should poll nimble agent get-generation every 10 seconds until status is success or failed, then publish automatically (or report failure). Present results to the user when done.
MCP Fallback (when CLI is not installed)
If nimble --version returns "command not found", fall back to the Nimble MCP server. All CLI commands have MCP equivalents — discover them via the MCP tool list. MCP tools accept the same parameters as CLI flags, passed as tool arguments instead of flags.
Parallel Execution
Make multiple Bash tool calls in a single response. Claude Code runs them in parallel automatically:
- Call 1:
nimble search --query "CompanyA news" --max-results 5 --search-depth lite - Call 2:
nimble search --query "CompanyB news" --max-results 5 --search-depth lite - Call 3:
nimble search --query "CompanyC news" --max-results 5 --search-depth lite
Sub-Agent Spawning
When using the Agent tool for parallel research:
- Always `mode: "bypassPermissions"` — sub-agents don't inherit Bash permissions.
- Batch max 4 agents. More risk hitting rate limits. For 5+, batch in groups.
- Tell agents to use Bash — explicitly say "Use the Bash tool to execute nimble
commands." Some agents try WebSearch instead.
- Fallback on failure — if any agent returns without results, run those searches
directly from the main context. Don't leave gaps.
Communication Style
Inform the user at phase transitions only with concrete numbers:
- "Researching Acme Corp + 5 competitors since Mar 12..."
- "Found 12 new signals. Pulling top 4 articles..."
- "All data collected. Building your briefing..."
Don't narrate individual tool calls.
Rate Limits & Common Errors
- Rate limit: 10 req/sec per API key
- Retry on 429: Reduce simultaneous calls
- Timeout: 30 seconds per request
| Error | Cause | Fix |
|---|---|---|
NIMBLE_API_KEY not set | Missing API key | See profile-and-onboarding.md |
401 Unauthorized | Expired key | Regenerate at app.nimbleway.com |
429 Too Many Requests | Rate limit | Fewer simultaneous calls |
timeout | Slow response | Retry once, then skip |
500 Server Error | Transient server failure | Retry once without --focus; if persistent, simplify query |
empty results | No matches | Remove --start-date, broaden query |
Signal Date Validation
High-quality intelligence requires distinguishing between when a page was published and when the underlying event occurred. This matters because:
- Syndicated or republished content may carry a different publication date than the
original source
- Secondary coverage (regulatory filings, recap articles, industry roundups) can
report on events that happened weeks or months earlier
Article Date vs Event Date
Every signal has two dates:
| What it is | |
|---|---|
| Article date | When the page was published |
| Event date | When the underlying event actually happened |
A signal is "new" only if its event date falls within the freshness window.
Event Date Extraction Rules
Sub-agents must determine the event date from content:
1. Explicit past reference — "launched in Q3", "appointed last October" → event date is in the past, regardless of the article date 2. Temporal language — "last quarter", "months ago", "earlier this year" → resolve relative to the article date 3. Present tense announcement — "today announces", "is launching" → event date ≈ article date 4. Dateline — "NEW YORK, March 15 —" → event date = that dateline date 5. If ambiguous — extract the source URL and check the on-page date
Source Type Hierarchy
When the same event appears from multiple sources, prefer those closest to the event:
1. Primary — the company's own domain, official press release, regulatory filing 2. Wire service — AP, Reuters, Bloomberg 3. Major outlet — original reporting with bylines 4. Derivative — syndicated copies, aggregator sites, recap articles, or content that attributes its information to another source
If the only source for a signal is derivative, corroborate against a primary or major source before reporting.
Freshness Classification
After determining the event date, classify each signal:
| Classification | Meaning | Action |
|---|---|---|
| NEW | Event date within freshness window, not in memory | Include in report |
| UPDATED | Known event with genuinely new information | Include as update |
| STALE | Old event covered by a recent article | DROP — do not include |
| UNCERTAIN | Can't determine event date from snippet alone | Extract URL to verify; if still uncertain after extraction, DROP |
Hard rule: Only signals classified as NEW or UPDATED may appear in reports. STALE and UNCERTAIN signals must be dropped entirely — not downgraded, not footnoted, not included as "background context." If a signal can't be verified as genuinely recent, it doesn't exist as far as the report is concerned.
--start-date Best Practices
--start-date is a useful filter for reducing noise, but always validate event dates from the content itself:
- For news queries (
--focus news), consider running a parallel undated query to
surface original sources alongside recent coverage
- The existing fallback ("If < 3 results, retry without
--start-date") remains useful
Verification Budget
Not every signal needs full verification — budget extract calls by priority:
| Priority | Examples | Verification |
|---|---|---|
| P1 (high impact) | Funding, M&A, leadership changes | Always extract + corroborate (see below) |
| P2 (medium impact) | Product launches, partnerships, major hires | Extract if date is UNCERTAIN or source is derivative |
| P3 (low impact) | Blog posts, minor hires, event appearances | Trust if date looks plausible; drop if obviously stale |
Skills define their own P1/P2/P3 signal types in their SKILL.md. The verification budget above applies universally regardless of which signals a skill classifies at each level.
P1 Corroboration (Mandatory)
Any P1 signal sourced from derivative or aggregator sites must be corroborated before it can appear in a report. This is a hard gate, not a suggestion.
For each P1 signal that needs corroboration:
nimble search --query "[Company] [event summary]" --max-results 5 --search-depth liteLook for the primary source (company blog, press release, official filing, regulatory document). Check the primary source's date:
- Primary source dates the event within the freshness window → signal is NEW, include it
- Primary source dates the event outside the freshness window → reclassify as STALE, drop
- No primary source found → reclassify as UNCERTAIN, drop
Do not report P1 signals that fail corroboration. It's better to miss a real signal than to report a stale one as new — trust is the product.
---
Entity Deduplication
When a skill collects entity records from multiple sources (directories, search results, extracted pages), deduplicate before reporting. This is distinct from signal-level differential analysis (see memory-and-distribution.md) — entity dedup merges records for the same entity across sources within a single run.
Three-layer pattern (generic — each skill customizes the specifics):
1. Exact ID match — If the entity type has a canonical ID (place_id, NPI number, domain), match on that first. Exact match = same entity, merge fields. 2. Domain normalization — Strip www., trailing slashes, protocol. Compare root domains. www.acme.com/ and acme.com are the same entity. 3. Fuzzy name + location — Normalize names before comparing:
- Lowercase all characters
- Strip titles and honorifics (
Dr.,Mr.,Ms., etc.) - Strip credential suffixes (
MD,DDS,Inc,LLC,Corp, etc.) - Strip common noise words (
The,and,of,&) - Collapse whitespace and punctuation
- Compare normalized names with location context if available
This catches cross-source variations like "Dr. Jane Smith, MD" (Maps) vs "Jane Smith" (Yelp) vs "Smith Eye Care LLC" (BBB). Each source formats names differently — always normalize before comparing.
Track source_count per entity — entities confirmed by multiple sources are higher quality. Each skill defines which layers apply and any domain-specific matching rules in its reference files.
---
Entity Confidence Scoring
Rate each entity's data completeness so users know what to trust.
Generic formula — each skill defines its own target field list (N fields):
- High — All target fields found + confirmed by 2+ sources (
source_count >= 2) - Medium — >50% of target fields found
- Low — ≤50% of target fields found
Display the confidence level in output (e.g., ⬤⬤⬤ High, ⬤⬤○ Medium, ⬤○○ Low). Each skill defines its field list and may add criteria (e.g., requiring a verified phone number for High in a provider directory skill).
---
Input Parsing Pattern
Skills that accept batch input (lists of URLs, companies, locations) should detect the input type automatically:
| Input signature | Type | Action |
|---|---|---|
Contains docs.google.com/spreadsheets | Google Sheet URL | Read sheet directly |
Path ends in .csv and file exists | CSV file | Read and parse as CSV |
| Contains multiple URLs (one per line or comma-separated) | Inline URL list | Parse directly |
| Otherwise | Unknown | Ask user for input |
Normalize all inputs to a uniform list of records before batch processing. Don't assume a specific format — detect and adapt.
---
Scaled Execution
When a skill needs to run multiple WSA or API calls, choose the execution tier based on the estimated number of requests. Each skill calculates its own estimate from input size and operations per record.
| Estimated calls | Strategy | How |
|---|---|---|
| 1–10 | Individual calls | Parallel Bash calls (max 4 concurrent) |
| 11–100 | Single batch | extract-batch or agent run-batch — one API call, server-side parallelism, poll for results |
| 100–1,000 | Multiple batches | Split into batches of up to 1,000. Use sub-agents to prepare inputs and process results |
| >1,000 | Confirmation gate + batches | Show estimate, ask user to confirm before proceeding, then execute via batches |
Individual calls (1–10)
Run up to 4 concurrent Bash calls per the Parallel Execution rules above.
Batch calls (11+)
For page extraction (11+ URLs):
nimble extract-batch \
--shared-inputs 'format: markdown' \
--input '{"url": "https://example.com/page-1"}' \
--input '{"url": "https://example.com/page-2"}'Add --shared-inputs 'render: true' if pages need JavaScript rendering.
For WSA/agent calls (11+ entities):
nimble agent run-batch \
--shared-inputs 'agent: {agent_name}' \
--input '{"params": {...}}' \
--input '{"params": {...}}'Both return a batch_id. Poll progress:
nimble batches progress --batch-id {batch_id}Fetch results when complete:
nimble batches get --batch-id {batch_id}
nimble tasks results --task-id {task_id}Batch API handles up to 1,000 requests per call with server-side orchestration. For >1,000 requests, split into multiple batch calls.
Sub-agents should also batch. When spawning sub-agents for parallel work, tell each agent to use extract-batch or agent run-batch for its assigned items rather than making individual calls. One batch call per agent is faster and more reliable than 5-6 sequential calls.
Large job confirmation (>1,000)
Before executing, show the estimate and ask the user to confirm:
Estimated API calls: ~2,400 (120 locations × 3 WSAs per location × ~7 enrichment)
This is a large job. Proceed? [Y/n]Pattern: estimate → display → gate → execute
Why batch over individual calls
Individual nimble agent run calls each require a separate HTTP round-trip and Bash tool invocation. At scale (dozens+) this is slow, unreliable, and wasteful on a local machine. Batch APIs move orchestration server-side — one API call triggers all requests, and you poll for results. Always prefer batch when above the individual threshold.
---
Query Construction Tips
- Be specific: "Acme Corp product launch 2026" > "Acme Corp"
- Use `--include-domain '["domain"]'` for companies with generic names
- Fallback on empty: If < 3 results, retry without
--start-date - Combine focus modes: news + general in parallel for broader coverage
- Try variations: "CompanyName" → "Company Name" → domain
Profile & Onboarding
The business profile at ~/.nimble/business-profile.json and first-run setup flow.
---
Profile Schema
{
"company": {
"name": "Acme Corp",
"domain": "acme.com",
"description": "Enterprise SaaS platform for project management"
},
"industry_keywords": ["project management software", "team collaboration SaaS"],
"competitors": [
{ "name": "WidgetCo", "domain": "widgetco.com", "category": "project-mgmt" },
{ "name": "GizmoTech", "domain": "gizmotech.io", "category": "project-mgmt" }
],
"preferences": {
"skip_competitors": [],
"output_format": "bullet-points"
},
"integrations": {
"notion": { "reports_page_id": "" },
"slack": { "channel": "" }
},
"sales_context": {
"key_differentiators": [
"Only platform with real-time web data access",
"Sub-second API response times"
],
"integration_partners": [
{ "name": "DataStack", "type": "data warehouse" },
{ "name": "CRMHub", "type": "CRM" }
],
"case_studies": [
{ "customer": "Large enterprise retailer", "industry": "retail", "outcome": "3x faster competitive intel" }
],
"common_objections": [
{ "objection": "We already use [competitor]", "response": "Our real-time data is fresher — most competitors cache for 24h+" }
]
},
"last_runs": {
"competitor-intel": "2026-03-20T14:30:00Z",
"meeting-prep": "2026-03-22T09:00:00Z"
},
"setup_completed": true
}Reading the Profile
At the start of every skill run:
cat ~/.nimble/business-profile.json 2>/dev/nullIf missing or empty → trigger onboarding (see below).
Key fields:
company.name/company.domain— the user's companycompetitors— tracked competitors with domains and categoriesindustry_keywords— for industry-level searchespreferences.skip_competitors— competitors to excludelast_runs.{skill-name}— timestamp for time-aware searchessales_context— value positioning data (differentiators, integrations, case studies, objections)integrations— Notion/Slack config for report distribution
Updating the Profile
After every skill run — update last_runs:
import json, datetime, os
path = os.path.expanduser("~/.nimble/business-profile.json")
with open(path, "r") as f:
profile = json.load(f)
profile["last_runs"]["skill-name"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
with open(path, "w") as f:
json.dump(profile, f, indent=2)On user correction — apply immediately:
| User says | Action |
|---|---|
| "Don't include CompanyX" | Add to preferences.skip_competitors |
| "Also track CompanyY" | Add to competitors (with domain + category) |
| "I moved to NewCompany" | Update company |
| "Show me more detail" | Update preferences.output_format |
Always confirm: "Got it — removed CompanyX from tracking."
Rules:
- Never overwrite the whole file. Read → modify → write.
- Preserve unknown fields.
- Handle missing file gracefully → trigger onboarding.
- JSON only, always valid.
---
First-Run Onboarding
Prerequisite Checks
The transport selection in nimble-playbook.md determines whether CLI or MCP is active. This section covers the install/upgrade/auth flow when neither is ready.
Minimum CLI version: 0.12.0
Preferred path — any Claude product (Claude Code, Claude Cowork, claude.ai)
The plugin install is one command and handles MCP registration + OAuth automatically:
"Run /plugin install nimble to install the Nimble plugin. The plugin's MCPserver auto-registers as a Connector you can see in Customize → Connectors.On first use, the OAuth flow runs in your browser — no API key needed."
This works in every Claude product (Code, Cowork, claude.ai) — they share the plugin + connector mechanism.
Plugin installed but connector not connected (Cowork / claude.ai)
The most common Cowork / claude.ai failure: the plugin is installed (mcp__plugin_nimble_nimble__* tools are listed) but its connector isn't connected, so live data calls fail. Check this before doing any work — don't fire a data call and react to the error. A single read-only nimble_agents_list probe confirms it: success = connected, proceed; auth/not-connected error or a response containing an OAuth authorization URL = not connected.
When not connected, tell the user verbatim and stop — never fall back to WebFetch, WebSearch, or any other tool, and never guess at data:
Your Nimble plugin is installed, but its connector isn't connected yet — that's
why live data isn't working. To connect it:
>
1. Open Customize → Connectors
2. Find Nimble and click Connect
3. Complete the login in your browser. No Nimble account? You can create one
right there during login.
4. Once it shows Connected, re-run your request.
If a tool returns an OAuth "Authorize" link instead of data, present the link as-is and stop. Do not invent a completion step ("paste the URL back", "I'll complete the connection") — no such step exists. Do not claim the tools will activate and then call them in the same turn. Wait for the user to authorize, then retry (or run one nimble_agents_list probe to confirm).
Codex CLI or other terminal agents (shell available, no /plugin install)
When /plugin install isn't available but the user has shell access, install the CLI directly — it exposes the full Nimble surface area:
1. Check if npm is available: npm --version 2. If npm exists:
"The Nimble CLI is required. I'll install it now."
>
Run: npm install -g @nimble-way/nimble-cli3. If npm is not available:
"The Nimble CLI requires Node.js/npm. Install Node.js first from
nodejs.org, then run: npm install -g @nimble-way/nimble-cli"4. After install, verify: nimble --version 5. If verification fails, stop and ask the user to check their PATH.
Cursor, VS Code, or other MCP clients outside the Claude family
When neither /plugin install nor shell access is workable, have the user paste this into their MCP settings (e.g., .cursor/mcp.json or the host's equivalent):
{
"mcpServers": {
"nimble": {
"type": "http",
"url": "https://mcp.nimbleway.com/mcp"
}
}
}After install, the first tool call triggers the OAuth flow automatically.
CLI outdated (version < 0.12.0)
Parse the version from nimble --version. If below 0.12.0:
"Your Nimble CLI is version [current] — version 0.12.0+ is required
for these skills. Upgrading now..."
>
Run: npm update -g @nimble-way/nimble-cliVerify after upgrade: nimble --version. If still outdated, suggest: npm uninstall -g @nimble-way/nimble-cli && npm install -g @nimble-way/nimble-cli
API key not set
You need a Nimble API key.
1. Go to app.nimbleway.com → API Keys
2. Generate a new key
3. Run: export NIMBLE_API_KEY=your_key_here4. Add to~/.zshrcor~/.bashrcto make permanent.
After the user sets it, verify: echo "NIMBLE_API_KEY=${NIMBLE_API_KEY:+set}"
API key expired (401)
Your key may have expired (72h TTL). Regenerate at app.nimbleway.com > API Keys.
All prerequisites met
Only proceed to Company Setup once CLI is installed, version is >= 0.12.0, and API key is set. Don't silently skip any check.
Company Setup (2 prompts max)
Prompt 1 — ask in plain text (NOT AskUserQuestion with options):
"What's your company's website domain? (e.g., acme.com)"
Verify — make two Bash calls simultaneously:
nimble search --query "[domain]" --include-domain '["[domain]"]' --max-results 3 --search-depth litenimble search --query "[domain] company" --max-results 5 --search-depth lite
Present what you found and confirm: "I found that [Company] ([domain]) is [brief description]. Is this the right company?"
Prompt 2 — skill-specific setup:
- competitor-intel: Offer choice via
AskUserQuestion: - Find for me — search and suggest competitors
- I'll list them — user provides names
If "Find for me", make three Bash calls simultaneously:
nimble search --query "[Company] competitors" --max-results 10 --search-depth litenimble search --query "[Company] vs" --max-results 10 --search-depth litenimble search --query "[Company] alternatives" --max-results 5 --search-depth lite
- meeting-prep: No extra setup — context comes per-meeting
- company-deep-dive: No extra setup — target company comes per-request
Create Profile
mkdir -p ~/.nimble/memory/{competitors,people,companies,reports,positioning,synthesis}Write ~/.nimble/business-profile.json using the schema above.
When setting up competitors, infer or ask for each competitor's domain and category. Also infer industry keywords from the company description.
Profile Exists
Skip onboarding. Greet with context: "Running competitor intel for Acme Corp — tracking WidgetCo, GizmoTech."
---
Error Recovery
If any step fails: 1. Tell the user what went wrong in plain language 2. Provide the exact command to fix it 3. Offer to retry
Never silently skip setup steps.
SEO Audit Checks
Detailed rules, severity logic, and parser schema for the seo-site-audit skill.
---
Parser Schema
Use this JSON schema with nimble extract --parse --parser '{...}' to extract structured SEO fields from each page. Pass this schema to sub-agents via the {parser_schema} placeholder.
{
"title": "The content of the <title> tag",
"meta_description": "The content attribute of <meta name='description'>",
"canonical": "The href attribute of <link rel='canonical'>",
"og_title": "The content attribute of <meta property='og:title'>",
"og_description": "The content attribute of <meta property='og:description'>",
"twitter_card": "The content attribute of <meta name='twitter:card'>",
"h1": "A JSON array of all H1 element text contents on the page",
"h2_h6_outline": "A hierarchical outline of H2-H6 headings as nested text",
"schema_jsonld": "A JSON array of all @type values found in <script type='application/ld+json'> blocks",
"internal_link_count": "Count of <a> links pointing to the same domain",
"external_link_count": "Count of <a> links pointing to external domains",
"img_without_alt_count": "Count of <img> tags missing the alt attribute or with empty alt",
"word_count": "Total word count of visible body text (excluding nav, footer, scripts)",
"has_hreflang": "true if any <link rel='alternate' hreflang='...'> tags exist, false otherwise",
"lang_attr": "The value of the lang attribute on the <html> element",
"canonical_self_referential": "true if the canonical URL matches the current page URL, false otherwise"
}When the parser returns nulls for all fields on a page, escalate the render tier and retry before recording the page as "extraction failed."
Fields NOT available via the parser:
status_code— HTTP response status comes from the extract response metadata
(the status_code field in the API response JSON), not from parsed page content. Read it from the response envelope, not the parser output.
content_to_html_ratio— requires access to the raw HTML byte count, which the
parser does not provide. Estimate it by comparing word_count (from parser) to the total response size (from response metadata). If response metadata lacks size, skip this check and note it as "not measurable."
---
Category 1: Meta Tags
| # | Rule | Condition | Severity |
|---|---|---|---|
| 1.1 | Missing title | title is null or empty | Critical |
| 1.2 | Duplicate title | Same title value on 2+ pages | High |
| 1.3 | Title too short | title length < 30 characters | Medium |
| 1.4 | Title too long | title length > 60 characters | Medium |
| 1.5 | Missing meta description | meta_description is null or empty | Medium |
| 1.6 | Duplicate meta description | Same meta_description on 3+ pages | High |
| 1.7 | Meta description too short | meta_description length < 70 characters | Low |
| 1.8 | Meta description too long | meta_description length > 160 characters | Low |
| 1.9 | Missing canonical | canonical is null or empty | Medium |
| 1.10 | Non-self-referential canonical | canonical_self_referential is false | Medium |
| 1.11 | Missing OG tags | og_title or og_description is null | Low |
| 1.12 | Missing Twitter Card | twitter_card is null or empty | Low |
Recommendations:
- 1.1: Add a unique, descriptive
<title>tag to every page. Include primary keyword. - 1.2: Write unique titles for each page reflecting its specific content.
- 1.3/1.4: Aim for 30–60 characters. Include primary keyword near the start.
- 1.5: Add a meta description summarizing page content in 70–160 characters.
- 1.6: Write unique descriptions. Shared descriptions signal thin content to search engines.
- 1.9/1.10: Add a self-referential canonical tag to prevent duplicate content issues.
- 1.11/1.12: Add OG and Twitter Card meta tags for better social sharing previews.
---
Category 2: Heading Structure
| # | Rule | Condition | Severity |
|---|---|---|---|
| 2.1 | Missing H1 | h1 array is empty | Critical |
| 2.2 | Multiple H1s | h1 array has 2+ elements | High |
| 2.3 | Hierarchy gap | H3 appears without a preceding H2, or H4 without H3, etc. (from h2_h6_outline) | Medium |
| 2.4 | Empty heading | Any heading in the outline has empty or whitespace-only text | Low |
| 2.5 | Duplicate H1 | Same H1 text on 2+ pages (excluding homepage) | Medium |
Recommendations:
- 2.1: Every page needs exactly one H1 describing the page's primary topic.
- 2.2: Use a single H1 per page. Use H2–H6 for subsections.
- 2.3: Maintain proper heading hierarchy. Don't skip from H1 to H3.
- 2.4: Remove empty heading tags or add meaningful text.
- 2.5: Write unique H1s reflecting each page's distinct content.
---
Category 3: Schema Markup (JSON-LD)
| # | Rule | Condition | Severity |
|---|---|---|---|
| 3.1 | No JSON-LD on content page | schema_jsonld is empty on a page with word_count > 300 | High |
| 3.2 | Missing required fields | JSON-LD @type present but lacks required properties for that type (Article needs headline + datePublished; Product needs name + offers; Organization needs name + url) | Medium |
| 3.3 | Deprecated @type | schema_jsonld contains deprecated types (e.g., DataCatalog for datasets) | Low |
| 3.4 | No Organization schema on homepage | Homepage schema_jsonld doesn't include Organization or WebSite | Medium |
Recommendations:
- 3.1: Add JSON-LD structured data matching the page content type (Article, Product,
FAQ, etc.). This enables rich snippets in search results.
- 3.2: Fill in all required properties per schema.org type definitions.
- 3.3: Update to current schema.org types.
- 3.4: Add Organization and WebSite schema to the homepage for brand knowledge panel.
---
Category 4: Internal Links
| # | Rule | Condition | Severity |
|---|---|---|---|
| 4.1 | Orphan page | Page has 0 internal inlinks (discovered via sitemap but not linked from any crawled page) | Critical |
| 4.2 | Broken internal link | A linked internal URL returns status_code >= 400 | High |
| 4.3 | Redirect chain > 2 hops | Internal link resolves through 3+ redirects | High |
| 4.4 | Excessive internal links | A single page has internal_link_count > 100 | Medium |
| 4.5 | Deep link depth | Page is > 4 clicks from the homepage (measured via link graph) | Medium |
| 4.6 | No internal links | internal_link_count is 0 on a content page | High |
Recommendations:
- 4.1: Add internal links to orphan pages from relevant parent or hub pages.
- 4.2: Fix or remove broken internal links. Update href to the correct URL.
- 4.3: Update links to point directly to the final destination URL.
- 4.4: Reduce internal links on mega-navigation pages. Prioritize contextual links.
- 4.5: Flatten site architecture. Important pages should be reachable in 3 clicks.
- 4.6: Add contextual internal links to related content pages.
---
Category 5: Content Quality
| # | Rule | Condition | Severity |
|---|---|---|---|
| 5.1 | Thin content | word_count < 300 on an indexable page (not a redirect, not noindex) | High |
| 5.2 | Duplicate content | Jaccard or 4-gram shingle similarity > 0.9 between two pages | High |
| 5.3 | Images missing alt text | img_without_alt_count > 0 on a content page | Medium |
| 5.4 | Low content-to-HTML ratio | content_to_html_ratio < 0.10 | Low |
| 5.5 | Very thin content | word_count < 100 on an indexable page | Critical |
Recommendations:
- 5.1/5.5: Expand thin pages with substantive content or consolidate with related pages.
- 5.2: Merge duplicate pages, implement canonical tags, or differentiate content.
- 5.3: Add descriptive alt text to all content images for accessibility and image SEO.
- 5.4: Reduce template bloat. Simplify HTML structure and remove unnecessary markup.
Duplicate detection approach: For each pair of pages in the same site section, compute 4-gram shingle sets from the body text and calculate Jaccard similarity. Pairs exceeding 0.9 are flagged. For large page sets (> 100 pages), compare within sections only to keep computation tractable.
---
Category 6: Technical Foundations
| # | Rule | Condition | Severity |
|---|---|---|---|
| 6.1 | robots.txt missing | Fetching {domain}/robots.txt returns 404 or empty | Critical |
| 6.2 | robots.txt blocks important paths | robots.txt disallows paths with high-value content (check for blanket Disallow: /) | Critical |
| 6.3 | Sitemap missing | No sitemap found at /sitemap.xml or referenced in robots.txt | Critical |
| 6.4 | Sitemap stale | Sitemap <lastmod> dates are > 90 days old | Medium |
| 6.5 | HTTPS not enforced | HTTP URLs don't redirect to HTTPS, or mixed content detected | Critical |
| 6.6 | Mixed content | Page loaded over HTTPS contains HTTP resources | High |
| 6.7 | URL uses underscores | URL path contains underscores instead of hyphens | Medium |
| 6.8 | URL mixed case | URL path contains uppercase characters | Medium |
| 6.9 | Session IDs in URLs | URL contains session-like parameters (?sid=, ?session=, ?jsessionid=) | Medium |
| 6.10 | Missing lang attribute | lang_attr is null or empty on the <html> element | Low |
| 6.11 | AI bots blocked | robots.txt disallows GPTBot, ClaudeBot, PerplexityBot, or ChatGPT-User — reduces AI visibility | Medium |
| 6.12 | No llms.txt | No /llms.txt file at site root — the emerging standard for AI agent discoverability | Low |
Recommendations:
- 6.1: Create a robots.txt file at the domain root.
- 6.2: Review Disallow rules. Ensure important content paths are crawlable.
- 6.3: Generate and submit an XML sitemap. Reference it in robots.txt.
- 6.4: Update sitemap
<lastmod>dates to reflect actual content changes. - 6.5: Enforce HTTPS site-wide via server redirects (301).
- 6.6: Update all resource references to use HTTPS or protocol-relative URLs.
- 6.7/6.8: Use lowercase hyphens in URLs. Set up 301 redirects from old URLs.
- 6.9: Remove session IDs from URLs. Use cookies for session tracking instead.
- 6.10: Add
lang="en"(or appropriate language code) to the<html>element. - 6.11: Allow AI bots in robots.txt unless there's a specific reason to block.
Blocking GPTBot/ClaudeBot/PerplexityBot reduces visibility in AI-generated answers. See references/ai-platform-profiles.md for the full AI bot user-agent list.
- 6.12: Add an
/llms.txtfile describing your site structure and capabilities for
AI agents. See llmstxt.org for the spec. Low priority but forward-looking.
---
Category 7: Core Web Vitals (Observational)
These checks are inferred from page content — not measured with Lighthouse. They flag likely CWV problems based on observable HTML patterns.
| # | Rule | Condition | Severity |
|---|---|---|---|
| 7.1 | Large images without lazy loading | <img> with src pointing to an image likely > 500KB (based on URL patterns like high-res filenames) and no loading="lazy" attribute | Medium |
| 7.2 | Render-blocking resources | <link rel="stylesheet"> or <script> in <head> without async/defer attributes | Medium |
| 7.3 | Excessive DOM depth | DOM nesting exceeds 32 levels (inferred from heading/div nesting in extracted HTML) | Low |
| 7.4 | No viewport meta tag | Missing <meta name="viewport"> tag | Medium |
Recommendations:
- 7.1: Add
loading="lazy"to below-the-fold images. Serve images in modern formats
(WebP/AVIF) and use responsive srcset.
- 7.2: Add
asyncordeferto non-critical scripts. Inline critical CSS. - 7.3: Simplify DOM structure. Reduce unnecessary wrapper elements.
- 7.4: Add
<meta name="viewport" content="width=device-width, initial-scale=1">.
---
Finding Confidence Tiers
Every finding must be tagged with a confidence level based on how it was detected:
| Tier | Label | Meaning | When to assign |
|---|---|---|---|
| Confirmed | [C] | Measured by parser or script — deterministic | Parser returned the field; value was checked against a rule |
| Likely | [L] | Strong signal from markdown/content analysis | Inferred from markdown extraction (e.g., heading count, word count); no parser verification |
| Hypothesis | [H] | Possible issue, needs manual verification | Could not verify from available data (e.g., schema detected in markdown text but not validated, redirect chain inferred from URL patterns) |
Display in the report: [C] Missing title tag on /about vs [H] Possible missing JSON-LD — could not parse from markdown-only extraction.
Rules by category (assuming extraction uses --format html + --format markdown or --parse --parser):
- Meta Tags (1.x): Confirmed — all tags (title, meta description, canonical,
og, twitter) parseable from data.html <head>. Hypothesis only if HTML extraction failed entirely (empty <head>).
- Heading Structure (2.x): Confirmed — parseable from both HTML and markdown.
- Schema Markup (3.x): Confirmed —
<script type="application/ld+json">
blocks parseable from data.html. Hypothesis only if HTML extraction failed.
- Internal Links (4.x): Likely — link counts from markdown are approximate
(nav boilerplate inflates counts); Confirmed when using HTML body section.
- Content Quality (5.x): Confirmed for word count from markdown; Likely for
duplicate detection (shingle analysis depends on clean text extraction).
- Technical Foundations (6.x): Confirmed for robots.txt/sitemap/llms.txt
(directly fetched); Confirmed for lang_attr (from <html lang>) and hreflang; Hypothesis for HTTPS/mixed content enforcement (inferred from URLs).
- Core Web Vitals (7.x): Hypothesis — all CWV checks are observational estimates.
---
Severity Escalation Rules
- Widespread duplication: If the same issue (same rule number) affects > 10% of
audited pages, bump severity by one level (Low → Medium, Medium → High, High → Critical). Critical stays Critical.
- Homepage penalty: Issues found on the homepage are NOT auto-escalated — they
carry the same base severity as any other page. The homepage is important, but the rule severity already reflects the issue's impact.
- Combined impact: If a page has 3+ distinct Critical/High issues, flag it as a
"high-priority fix page" in the Quick Wins section.
---
Dedup Against Prior Audit
When a prior findings-{date}.json exists for this domain:
1. Build a signature for each finding: {page_url}|{rule_number} (e.g., https://acme.com/blog|1.1) 2. Compare each new finding's signature against the prior findings 3. Classify:
- Signature exists in prior AND severity is the same → Unchanged
- Signature exists in prior AND severity is worse → Worsened
- Signature does NOT exist in prior → New
- Prior signature does NOT exist in current → Resolved
Report placement:
- New and Worsened findings → TL;DR, Critical/High/Medium/Low issue tables
- Unchanged findings → Category Breakdown sections only (with "unchanged" tag)
- Resolved findings → noted as improvements in the TL;DR ("3 issues resolved
since last audit")
This keeps the executive summary focused on what changed while preserving the full picture in category breakdowns.