references/memory-and-distribution.md
# 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 jargon
```
**Tier 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.md
```
Skills 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.
```markdown
# 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.
```markdown
# 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 `.md` extension) 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?"
```markdown
# 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 CloudCorp
```
### Rules
- **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` — enables `grep "^## \[" 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
```markdown
# Alex Kim
## Current Role
VP of Engineering at [[competitors/widgetco]] (since 2024)
## Related
- Employer: [[competitors/widgetco]]
- Previous: [[companies/cloudcorp]]
```
```markdown
# WidgetCo
## Key People
- [[people/alex-kim]] — VP Engineering
- [[people/jane-smith]] — CEO
## Related Competitors
- [[competitors/gizmotech]] — overlapping market segment
```
### Rules
- **Link format:** `[[directory/entity-slug]]` — no `.md` extension, 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:**
```markdown
## 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):
```markdown
## [2026-03-22] ad-hoc-insight
- Updated: [[competitors/widgetco]], [[competitors/gizmotech]]
- Key findings:
- WidgetCo enterprise pricing is 2x user's, GizmoTech closer to parity
```
### Rules
- **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.
```markdown
---
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).
```markdown
# 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 `## Open` section 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 `## Resolved` with 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.
```markdown
# 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.
```markdown
# 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 summaries
```
### companies/
Detailed company profiles from deep-dive research.
```markdown
# 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)
```bash
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:
```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_runs` timestamp.
- **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 search` in the result's `url` field
- 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.
references/nimble-playbook.md
# 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-format` doesn'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 markdown` to 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` (>= 1.2.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 |
| `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_list` call 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.json` inside the plugin auto-registers as a Connector in `Customize → 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
> ```
> Then `export NIMBLE_API_KEY=<key>` and re-run. See `references/profile-and-onboarding.md` for 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 carry a stable integration attribution so usage from this plugin
can be tracked. The value is always `nimble-agent-skills`.
**CLI path** — add `--client-source nimble-agent-skills` 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 (or set
`CLIENT_SOURCE=nimble-agent-skills` in the environment, which the CLI reads automatically):
```bash
nimble --client-source nimble-agent-skills search --query "..."
nimble --client-source nimble-agent-skills extract --url "..."
nimble --client-source nimble-agent-skills extract:templates run --template <name> --params '{...}'
nimble --client-source nimble-agent-skills agents:runs create --agent-id <id> --input "..."
nimble --client-source nimble-agent-skills map --url "..."
nimble --client-source nimble-agent-skills crawl run --url "..."
```
**MCP path** — integration attribution rides the CLI path via `--client-source`; MCP
requests are attributed at the transport level. Use the CLI path when per-integration
attribution matters. Don't add a header flag to override the MCP transport's attribution.
## 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:
```bash
ls ~/.nimble/memory/reports/{upstream-skill}-*$(date +%Y-%m-%d).md 2>/dev/null
```
Use 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
```bash
# 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 10
```
**Key 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_runs` timestamp from business profile
- If < 3 results: retry without `--start-date`
## Extract
```bash
# 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 markdown
```
Response 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
```bash
# 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
```bash
nimble map --url "https://example.com/blog" --limit 20
```
### Site 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.
## Extraction Templates
Reusable, site-specific templates that return structured fields from a known site
(Amazon products, Reddit threads, Google Maps, etc.) — the right tool when you can point
directly at an item (by URL or identifier) and want clean parsed data, not raw page
content. Use **existing** templates only; do not build new ones from these skills. If no
template covers the site, fall back to `search` + `extract`, or use a Web Search Agent
(below) when the data needs discovery or reasoning across pages.
```bash
# List templates (paginated; scan display_name / name / metadata.domain)
nimble extract:templates list --limit 100
# Inspect a template's input_schema + output_schema before running
nimble extract:templates get --extract-template-name <template_name>
# Run a template (realtime). --params is a JSON/YAML mapping matching input_schema
nimble extract:templates run --template <template_name> --params '{"key": "value"}'
# Async (returns a task to poll) and batch (up to 1,000 items)
nimble extract:templates async --template <template_name> --params '{"key": "value"}'
nimble extract:templates batch --template <template_name> \
--input '{"params": {"key": "value-1"}}' \
--input '{"params": {"key": "value-2"}}'
```
**Key flags:**
- `--template` — template `name` from `extract:templates list` (required for run/async/batch)
- `--extract-template-name` — template `name` (for `get`)
- `--params` — JSON/YAML mapping of inputs, matching the template's `input_schema` (required)
- `--localization` — enable zip_code/store_id localization (template-dependent)
**Response:** the structured records defined by the template's `output_schema` — an array
for list/SERP-style templates, an object for detail/PDP-style templates. Always read the
`output_schema` from `extract:templates get` to know the shape before parsing. REST/SDK
equivalent: `POST /v2/extract/templates/run` (and `/async`, `/batch`).
**Async task states:** `pending` → `success` or `error`. Poll status with
`nimble tasks get --task-id <task_id>` until terminal, then fetch with
`nimble tasks results --task-id <task_id>`; batches with `nimble batches progress`.
## Web Search Agents
AI-driven agents for open-ended web work — **research, data enrichment, and dataset
building** — where the source isn't fixed, data is scattered across pages, structure is
inconsistent, or a synthesized answer is needed. Given a goal, an agent discovers where
the information lives, navigates to it, and returns structured or written output with
per-claim citations. This is the right tool when an Extraction Template doesn't fit
because there's no single known page to parse (see the routing note above).
**Reuse-priority — check in this order before creating a new agent:**
1. An existing agent in the account already covers this (`agents list`).
2. A close-match **agent template** worth materializing (`agents:templates list`).
3. Only if neither fits, create one from scratch.
Neither list command takes a server-side search term — list and filter client-side on
`agent_name` / `template_name` / `description` / `use_case`. Never hardcode names.
### Pick a run mode first
The identity you pass decides the route, and the route decides the command:
| Mode | Identity | Command |
| ----------------------------- | --------------------------- | ------------------------------------------- |
| **1 — named create-or-reuse** | `--agent-name`, no agent ID | `nimble agents run --agent-name <name>` |
| **2 — explicit agent** | `--agent-id` | `nimble agents:runs create --agent-id <id>` |
| **3 — caller-anonymous** | neither | `nimble agents run` |
**Mode 1 is the default for these skills** — derive a deterministic name (`{skill}-{purpose}`)
so repeat sessions reuse the same agent instead of creating near-duplicates. A repeated name
returns the **same `web_search_agent_id`**. `agents:runs create` **requires** `--agent-id`
and ignores `--agent-name`; use `nimble agents run` for Modes 1 and 3. Mode 3 still returns a
generated `web_search_agent_id` — keep it, `get` and `result` both need it.
```bash
# Discover pre-built agent templates, then inspect one
nimble --client-source nimble-agent-skills agents:templates list
nimble --client-source nimble-agent-skills agents:templates get --template-name <template_name>
# Create an agent up front — from a template, or from scratch
nimble --client-source nimble-agent-skills agents create --template <template_name>
nimble --client-source nimble-agent-skills agents create \
--display-name "<name>" --goal "<goal>" --sources '{...}' \
--output-schema '{...}' --use-case research --effort high
# Mode 1 run (async), then poll status and fetch the result
nimble --client-source nimble-agent-skills agents run \
--agent-name "<skill>-<purpose>" --use-case research \
--input "<task or question>" --effort high
nimble --client-source nimble-agent-skills agents:runs get --agent-id <agent_id> --run-id <run_id>
nimble --client-source nimble-agent-skills agents:runs result --agent-id <agent_id> --run-id <run_id>
```
**Run controls** (both run commands): `--input` (required), `--effort`
(`low`/`medium`/`high`/`x-high`/`max` — default `high` once several fields need real digging),
`--output-schema`, `--input-data`, `--sources`, `--enable-events`,
`--previous-interaction-id`, plus `--skill` and `--use-case` per the rules below.
- **`--sources`** has two shapes in one object: `allow` / `block` are arrays of groups
(`title` required, `domains`, optional `order` for priority); `prioritize` / `avoid` are
plain guidance strings.
- **`--input-data`** carries the rows you already have; `--output-schema` describes the shape
of the answer. Enriching several rows needs an **array** schema — an object schema returns
one object. Carried-in fields come back with `confidence: "pre_existing"` and no citations;
never present them as sourced findings.
### `use_case` locks; `skill` overrides once
`use_case` is exactly `research`, `enrichment`, or `dataset_building`. It is stored when the
agent is **created** (including a Mode 1 first call or a Mode 3 run). Against an existing
agent the same value is a no-op and a different value is **rejected** — omit it, match it, or
use a different agent. `dataset_building` additionally requires an `--output-schema` and
effort `high` or above.
`--skill` on a run against an **existing** agent applies to that run only and leaves stored
config untouched. On the call that **creates** the agent, `--skill` and `--use-case` become
its stored configuration instead.
**Run lifecycle:** `queued` → poll `agents:runs get` until terminal (`completed`, `failed`,
`cancelled`) → fetch output with `agents:runs result`. Calling `result` early returns `409`
"Run still active" — go back to `get`, don't hammer `result`. The result's `output` is
`type: "text"` (prose) or `type: "json"` (structured), plus `trust` metadata with per-claim
citations. REST/SDK equivalent: `POST /v2/agents/*`.
**Live progress:** on the CLI, create with `--enable-events` and consume
`nimble agents:runs stream-events --agent-id <id> --run-id <id> [--max-items <n>]`. The stream
closes on its own at a terminal state and never carries the output — still fetch `result`
afterwards. **On MCP, use bounded status polling instead**: poll `nimble_agents_run_status`
every ~15–30s with a capped total wait, and report a run as still active rather than hanging
or calling it failed.
> Full contract (mode table, source shapes, trust metadata, error table):
> `skills/nimble-web-expert/references/nimble-agents/reference.md`.
**Fallback rule:** If neither an Extraction Template nor a Web Search Agent fits, fall
back to `nimble search` + `nimble extract`. Don't fail silently — log which domains
lacked coverage.
### Tasks & batches polling
```bash
# 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 tasks
```
**Workflow:** Always `extract:templates get` (or `agents:templates get`) before running,
to understand the expected input params and output fields.
> **Out of scope:** Building or publishing new Extraction Templates / Web Search Agents is
> not part of these skills — use **existing** templates and agents. Point users who need a
> custom template or agent to the Nimble app.
## 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.
Two Web Search Agent controls are CLI-only, each with a documented MCP alternative:
| CLI-only control | MCP alternative |
| ------------------------- | ---------------------------------------------------------------------- |
| `--enable-events` + `agents:runs stream-events` | Bounded `nimble_agents_run_status` polling (~15–30s, capped total wait) |
| `--previous-interaction-id` | Start a fresh run with the prior context restated in `input` |
| Mode 3 (no agent identity) | Pass an `agent_name` — `nimble_agents_run` requires `agent_id` or `agent_name` |
Modes 1 and 2, `use_case`, `skill`, `sources`, `output_schema`, `input_data`, and `effort`
work the same on both transports.
## 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:
```bash
nimble search --query "[Company] [event summary]" --max-results 5 --search-depth lite
```
Look 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 `extract:templates 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):**
```bash
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 structured template calls (11+ entities):**
```bash
nimble extract:templates batch \
--template {template_name} \
--input '{"params": {...}}' \
--input '{"params": {...}}'
```
Both return a `batch_id`. Poll progress:
```bash
nimble batches progress --batch-id {batch_id}
```
Fetch results when complete:
```bash
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 `extract:templates 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 extract:templates 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
references/profile-and-onboarding.md
# Profile & Onboarding
The business profile at `~/.nimble/business-profile.json` and first-run setup flow.
---
## Profile Schema
```json
{
"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:
```bash
cat ~/.nimble/business-profile.json 2>/dev/null
```
If missing or empty → trigger onboarding (see below).
Key fields:
- `company.name` / `company.domain` — the user's company
- `competitors` — tracked competitors with domains and categories
- `industry_keywords` — for industry-level searches
- `preferences.skip_competitors` — competitors to exclude
- `last_runs.{skill-name}` — timestamp for time-aware searches
- `sales_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`:
```python
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: 1.2.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 MCP
> server 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-cli`
3. If npm is not available:
> "The Nimble CLI requires Node.js/npm. Install Node.js first from
> [nodejs.org](https://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):
```json
{
"mcpServers": {
"nimble": {
"type": "http",
"url": "https://mcp.nimbleway.com/mcp"
}
}
}
```
After install, the first tool call triggers the OAuth flow automatically.
#### CLI outdated (version < 1.2.0)
Parse the version from `nimble --version`. If below 1.2.0:
> "Your Nimble CLI is version **[current]** — version **1.2.0+** is required
> for these skills. Upgrading now..."
>
> Run: `npm update -g @nimble-way/nimble-cli`
Verify 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](https://app.nimbleway.com) → API Keys
> 2. Generate a new key
> 3. Run: `export NIMBLE_API_KEY=your_key_here`
> 4. Add to `~/.zshrc` or `~/.bashrc` to 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 >= 1.2.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 lite`
- `nimble 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 lite`
- `nimble search --query "[Company] vs" --max-results 10 --search-depth lite`
- `nimble 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
```bash
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.
references/provider-extraction-patterns.md
# Provider Extraction Patterns
Skill-specific patterns for extracting practitioner data from healthcare practice
websites. For general extraction and site mapping rules, see `nimble-playbook.md`.
---
## Page URL Scoring
After `nimble map` discovers a site's pages, score each URL by path keywords to
identify provider-relevant pages. See Site Mapping Pattern in `nimble-playbook.md`
for the generic discover/score/filter/fallback flow.
### Keyword Weight Table
| Weight | Path Segments | Examples |
|--------|--------------|----------|
| **High** | `/providers`, `/doctors`, `/physicians`, `/our-team`, `/staff`, `/our-providers`, `/our-doctors`, `/our-physicians`, `/surgeons`, `/specialists` | `/our-providers`, `/meet-our-doctors` |
| **High** | `/dr-*`, `/doctor-*` (individual provider pages) | `/dr-jane-smith`, `/doctor-john-doe` |
| **Medium** | `/team`, `/about`, `/people`, `/about-us`, `/meet-the-team`, `/faculty`, `/clinicians` | `/about-us/team`, `/our-people` |
| **Low** | Homepage (`/`), `/services`, `/locations` | `/`, `/services/cataract-surgery` |
| **Skip** | `/blog`, `/news`, `/careers`, `/jobs`, `/privacy`, `/terms`, `/patient-portal`, `/pay-bill`, `/faq`, `/testimonials`, `/reviews`, `/gallery`, `/media` | `/blog/eye-health-tips` |
### Scoring Rules
- Cap at **15 pages** per practice site
- Always include at least one High-weight page (or homepage as fallback)
- Individual provider pages (`/dr-*`) are high value but cap at 10 per site to
avoid over-extraction on large multi-provider practices
- If `nimble map` returns < 3 scored candidates, fall back to:
```bash
nimble search --query "site:{domain} doctors OR providers OR team" --max-results 10 --search-depth lite
```
---
## Core Extraction Fields (5 Fields)
Every provider record targets these 5 fields. Confidence scoring uses this as the
N-field list (see Entity Confidence Scoring in `nimble-playbook.md`).
| # | Field | Key | Detection Patterns |
|---|-------|-----|-------------------|
| 1 | **Full Name** | `name` | `Dr.` prefix, `<h2>`/`<h3>` heading patterns, bold text near credential suffixes, structured bio sections |
| 2 | **Credentials** | `credentials` | Regex patterns (see below) found adjacent to names |
| 3 | **Specialty** | `specialty` | Keywords per vertical (see below), often near name or in bio paragraph |
| 4 | **Contact / Scheduling** | `contact` | Phone regex `\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}`, appointment URLs (`/book`, `/schedule`, `/request-appointment`), email addresses |
| 5 | **Education / Training** | `education` | "Residency", "Fellowship", "Medical School", "Board Certified", university names, graduation years |
### Confidence Scoring (from shared pattern)
- **High** -- 5/5 fields found + confirmed by 2+ pages or sources
- **Medium** -- 3-4/5 fields found
- **Low** -- 1-2/5 fields found
Display as: `High`, `Medium`, `Low`
---
## Credential Regex Patterns
Match these suffixes adjacent to provider names. Case-insensitive. Allow comma,
space, or period separators between multiple credentials.
### Medical Doctors
```
MD|M\.D\.|DO|D\.O\.
```
### Eye Care
```
OD|O\.D\.|FAAO
```
### Dental
```
DDS|D\.D\.S\.|DMD|D\.M\.D\.
```
### Advanced Practice
```
NP|PA|PA-C|ARNP|APRN|CNS|CRNA|DNP|D\.N\.P\.
```
### Therapy & Allied Health
```
PT|DPT|OT|OTR|SLP|CCC-SLP|RD|RDN|LCSW|LPC|PhD|Ph\.D\.|PsyD|Psy\.D\.
```
### Board Certifications (commonly listed)
```
FACS|FACP|FACC|FACOG|FAAP|FACEP|FAAOS|FASRS|FRCSC
```
### Combined Pattern
When scanning extracted markdown, look for names followed by credential clusters:
```
[Name],?\s*((?:MD|DO|OD|DDS|DMD|NP|PA|PA-C|ARNP|APRN|PT|DPT|PhD|FACS|FAAO|FACP|FACC|FACOG|FAAP|FAAOS|FASRS)[,.\s]*)+
```
---
## Specialty Keywords by Healthcare Vertical
### Ophthalmology
```
ophthalmology, ophthalmologist, retina, retinal, cataract, glaucoma, cornea,
corneal, LASIK, refractive surgery, oculoplastics, neuro-ophthalmology,
pediatric ophthalmology, vitreoretinal, anterior segment, posterior segment,
strabismus, ocular oncology, uveitis
```
### Dental
```
dentist, dentistry, general dentistry, cosmetic dentistry, orthodontics,
orthodontist, periodontics, periodontist, endodontics, endodontist,
oral surgery, oral surgeon, prosthodontics, prosthodontist, pediatric
dentistry, implants, dental implants, TMJ, sedation dentistry
```
### Dermatology
```
dermatology, dermatologist, Mohs surgery, cosmetic dermatology, skin cancer,
melanoma, psoriasis, eczema, acne, rosacea, laser treatment, botox,
fillers, chemical peel, phototherapy, patch testing
```
### General / Primary Care
```
family medicine, internal medicine, primary care, general practice,
preventive medicine, geriatrics, urgent care, walk-in clinic,
physical exam, wellness, annual checkup
```
### Orthopedics
```
orthopedics, orthopedic surgery, sports medicine, joint replacement,
spine surgery, hand surgery, foot and ankle, shoulder, knee,
arthroscopy, fracture care, physical therapy, rehabilitation
```
---
## Entity Deduplication (Skill-Specific)
Apply the shared 3-layer dedup pattern from `nimble-playbook.md` with these
skill-specific rules:
1. **Exact match** -- Same name + same practice domain = same provider
2. **Credential match** -- Same name + same credentials + same city = likely same
provider (even across different practice sites)
3. **Fuzzy match** -- Normalize names (strip "Dr.", middle initials, suffixes),
compare with Levenshtein distance <= 2 + same specialty = possible match,
flag for review rather than auto-merging
### Cross-source name normalization
Different sources format provider names very differently. Exact string matching
across sources produces near-zero matches. Always normalize before comparing:
| Source | Raw format | After normalization |
|--------|-----------|-------------------|
| Google Maps | "Dr. Jane A. Smith, MD - Retina Specialist" | "jane smith" |
| Yelp | "Jane Smith" | "jane smith" |
| BBB | "Smith Eye Care LLC" | "smith eye care" |
| Practice website | "Jane A. Smith, M.D., F.A.C.S." | "jane smith" |
Normalization steps (apply in order):
1. Strip titles: `Dr.`, `Mr.`, `Ms.`, `Prof.`
2. Strip credentials: all patterns from the Credential Regex section above
3. Strip business suffixes: `LLC`, `Inc`, `Corp`, `PC`, `PLLC`, `PA`, `Associates`
4. Strip specialty descriptors: "- Retina Specialist", "- Ophthalmologist"
5. Strip middle initials (single letters with optional period)
6. Lowercase, collapse whitespace, strip remaining punctuation
After normalization, match with location context (same city or same zip code).
For **practice-level** dedup (not provider-level), also try matching the practice
name against provider last names ("Smith Eye Care" → likely matches "Dr. Smith").
Track `source_count` -- providers found across multiple sources are higher
confidence than those from a single source.
references/wsa-reference.md
# Extraction Template Discovery for Healthcare Providers Enrich
How to find and evaluate Extraction Templates for enriching existing provider records. The Extraction Template catalog
evolves constantly — this skill discovers relevant agents at runtime rather than
relying on a static list.
For general Extraction Template execution rules (invocation, parsing, batch, fallback), see
`nimble-playbook.md`.
---
## Discovery Strategy
### Three search layers
Run these searches during preflight (Step 0) to build a session-specific Extraction Template
inventory. Run all searches simultaneously:
**Layer 1 — Vertical search:**
```bash
nimble extract:templates list --limit 100 # filter items for "healthcare"
```
Returns all agents tagged with the Healthcare vertical (clinical trials, FDA,
regulatory, and any newly added healthcare agents).
**Layer 2 — Session-specific search:**
Search for terms derived from the user's input — their specialty, specific
directories, or data sources they mentioned:
```bash
# If user's list is ophthalmologists:
nimble extract:templates list --limit 50 # filter items for "ophthalmology"
nimble extract:templates list --limit 50 # filter items for "eye"
# If user mentioned specific directories:
nimble extract:templates list --limit 50 # filter items for "healthgrades"
nimble extract:templates list --limit 50 # filter items for "zocdoc"
nimble extract:templates list --limit 50 # filter items for "npi"
```
Adapt search terms to whatever the user provided. Include the specialty, common
directory names for that specialty, and any data sources the user mentioned.
**Layer 3 — General enrichment tools:**
These Extraction Templates are useful across verticals for reputation, verification, and practice
details:
```bash
nimble extract:templates list --limit 50 # filter items for "google_maps"
nimble extract:templates list --limit 50 # filter items for "yelp"
nimble extract:templates list --limit 50 # filter items for "bbb"
nimble extract:templates list --limit 50 # filter items for "review"
```
### Evaluating discovered agents
For each discovered agent, read its `description` and `entity_type` to classify it
into an enrichment category:
| If the agent description mentions... | Assign to category |
|--------------------------------------|-------------------|
| Reviews, ratings, patient feedback, reputation | **Reputation** — practice/provider ratings |
| Clinical trials, FDA, regulatory, compliance, licensing | **Regulatory** — credentials and compliance data |
| Profile, detail page, business info, contact, hours | **Practice details** — supplementary practice info |
| Search, listings, directory, discovery | **Identity** — finding provider web presence |
Validate each relevant agent's params before using it:
```bash
nimble extract:templates get --extract-template-name [agent_name]
```
**Skip agents that don't fit** — not every healthcare-tagged agent is useful for
enrichment. A drug interaction agent, for example, isn't relevant for filling
provider contact info.
---
## Enrichment Phase Mapping
Unlike healthcare-providers-extract (which focuses on discovery and extraction),
this skill focuses on three enrichment categories. Identity search uses
`nimble search` directly — Extraction Templates add value in the enrichment phases.
### Identity (finding provider web presence)
**When:** Always — this is how you find bio pages for providers in the input list.
**Primary tool:** `nimble search` (not Extraction Template-dependent):
```bash
nimble search --query "[name] [credentials] [location] [specialty]" --max-results 5 --search-depth lite
```
**Extraction Template supplement:** If Layer 2 discovery found directory-specific agents (e.g., a
healthcare provider profile agent), use them for providers listed on that directory
to get structured data directly.
### Reputation enrichment
**When:** User requested reviews, ratings, or practice reputation data.
**Search terms for discovery:** `review`, `google_maps`, `yelp`, `bbb`
**How to use:** Run discovered review/rating agents with the provider's practice
name + location. Match results back to the provider record.
**Fallback:**
```bash
nimble search --query "[practice-name] [city] reviews ratings" --max-results 5 --search-depth lite
```
### Regulatory enrichment
**When:** User requested clinical trial activity, FDA data, board certification,
or accreditation status.
**Search terms for discovery:** `healthcare` vertical, `clinicaltrials`, `fda`,
`npi`, `license`, `board`
**How to use:** Run discovered regulatory agents with the provider's name and
credentials. Cross-reference results with existing provider data.
**Fallback:**
```bash
nimble search --query "[provider-name] [credentials] NPI OR license OR board certification" --max-results 5 --search-depth lite
```
### Practice details enrichment
**When:** User wants hours, insurance accepted, staff count, or other practice-level
data.
**Search terms for discovery:** Directory-specific agents found in Layer 2.
**Fallback:**
```bash
nimble search --query "[practice-name] [city] hours insurance" --max-results 5 --search-depth lite
```
Then extract the top result with `nimble extract --url "[url]" --format markdown`.
---
## Scaling Extraction Template Calls
Follow the Scaled Execution pattern from `nimble-playbook.md` — it covers
individual calls, batching, and the confirmation gate for large jobs.
For enrichment, estimate calls as: `[providers] x [enrichment categories requested]`.
A list of 50 providers with reputation + regulatory = ~100 Extraction Template calls = batch tier.
---
## Fallback Chain
If Extraction Template discovery returns nothing useful for a category, fall back to `nimble search`
+ `nimble extract` (the core Nimble tools always work):
1. **Identity fallback:** `nimble search --query "[name] [location] doctor" --max-results 5 --search-depth lite`
2. **Reputation fallback:** `nimble search --query "[practice-name] reviews" --max-results 5 --search-depth lite` + `nimble extract` on results
3. **Regulatory fallback:** `nimble search --query "[name] [credentials] NPI OR license OR board certification" --max-results 5 --search-depth lite`
The skill always produces results even if zero Extraction Templates are found — Extraction Templates accelerate
and enrich, but are never required.
SKILL.md
---
name: healthcare-providers-enrich
description: |
Fills gaps in existing healthcare practitioner lists — adds missing phone numbers,
credentials, specialties, contact info, education, reviews, and regulatory data.
Triggers: "enrich my provider list", "fill in missing data", "add phone numbers
to these doctors", "complete this practitioner database", "enrich CRM export",
"fill gaps in my provider data", "supplement this healthcare list".
Accepts CSV, Google Sheet URL, or pasted data. Searches for each provider's
practice website, extracts missing fields, and enriches with reviews, clinical
trials, and accreditation via WSAs.
Do NOT use for extracting providers from practice URLs — use healthcare-providers-extract instead.
Do NOT use for validating credentials — use healthcare-providers-verify instead.
Do NOT use for discovering practices — use market-finder or local-places instead.
Do NOT use for general extraction — use nimble-web-expert instead.
allowed-tools:
- Bash(nimble:*)
- Bash(date:*)
- Bash(cat:*)
- Bash(mkdir:*)
- Bash(python3:*)
- Bash(echo:*)
- Bash(jq:*)
- Bash(ls:*)
- Bash(wc:*)
- Read
- Write
- Edit
- Glob
- Grep
- Agent
- AskUserQuestion
metadata:
author: Nimbleway
version: 1.7.0
category: healthcare
---
# Healthcare Providers Enrich
Fill gaps in existing practitioner lists with verified web data, powered by Nimble's
web data APIs.
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).
---
## Instructions
### Step 0: Preflight + WSA Discovery
**Sibling handoff check:** Before running full preflight, check if
`healthcare-providers-extract` ran earlier in this session by following the Sibling
Handoff pattern from `references/nimble-playbook.md`. If same-day extract output
exists, skip CLI check and profile load, and reuse WSA Layer 1/3 inventory. Only
re-run Layer 2 if the specialty changed.
**Otherwise, run full preflight** from `references/nimble-playbook.md` (5 simultaneous
Bash calls: date calc, today, CLI check, profile load, index.md load).
**Also simultaneously** — run WSA discovery and setup:
- `mkdir -p ~/.nimble/memory/{reports,healthcare-providers-enrich/checkpoints}`
- `ls ~/.nimble/memory/healthcare-providers-enrich/checkpoints/ 2>/dev/null`
- Run Layer 1 (vertical) and Layer 3 (general tools) WSA discovery from
`references/wsa-reference.md`. Layer 2 (session-specific) runs after Step 1 when
you know the user's specialty.
Classify discovered agents into phases and validate with `nimble extract:templates get` per
`references/wsa-reference.md`.
From the preflight results:
- CLI missing or API key unset -> `references/profile-and-onboarding.md`, stop
- Tag all `nimble` CLI calls: `nimble --client-source nimble-agent-skills <subcommand>`. MCP requests are attributed at the transport level — see `references/nimble-playbook.md`.
- Profile exists -> note it for context. Determine mode using smart date windowing
from `references/nimble-playbook.md`:
- **Full mode:** first run OR last run > 14 days ago
- **Quick refresh:** last run < 14 days ago (re-enrich only records with gaps)
- **Same-day repeat:** if `last_runs.healthcare-providers-enrich` is today, check
for existing report at `~/.nimble/memory/reports/healthcare-providers-enrich-*[today].md`.
If found, ask: "Already ran today. Run again for fresh data?"
- No profile -> that's fine. This skill doesn't require onboarding. Proceed to Step 1.
### Step 1: Parse Input + Starting Questions
**Chained-from-extract shortcut:** Check for a same-day extract report:
```bash
ls ~/.nimble/memory/reports/healthcare-providers-extract-*$(date +%Y-%m-%d).md 2>/dev/null
```
If a same-day report exists, parse the `{slug}` from the filename and load
`~/.nimble/memory/healthcare-providers-extract/{slug}/providers.json`. The practice
domains and page URL patterns are already known — construct individual bio page URLs
from the site's URL convention and skip Step 3 entirely. This avoids N unnecessary
web searches. If no same-day report exists, do not reuse old `providers.json` files.
Parse `$ARGUMENTS` for input type using the Input Parsing Pattern from
`references/nimble-playbook.md`. Key routing:
- **Extract output detected** (providers.json) -> proceed to Step 2, mark Step 3 skip
- **CSV/Sheet/pasted data detected** -> proceed to Step 2
- **Unclear** -> ask (counts as 1 of max 2 prompts)
**If input is clear**, confirm and ask one shaping question (plain text, not
AskUserQuestion):
> "Found **N providers** in your list. Quick questions:
> 1. Which fields need filling? (contact info, credentials, specialty, reviews, regulatory — or all gaps)
> 2. Healthcare vertical? (ophthalmology, dental, dermatology, general, or other)"
**If input is ambiguous**, use AskUserQuestion (counts as 1 of max 2 prompts):
> **What provider list should I enrich?**
> - Paste provider data directly (name + any known info, one per line)
> - Provide a CSV file path or Google Sheet URL
> - Or describe what you have (e.g., "a list of 50 ophthalmologists with just names and states")
Skip questions the user already answered in their initial message.
### Step 2: Analyze Existing Data
Parse the input into structured records. For each provider, identify:
- **Known fields** — what the user already has (name, state, specialty, etc.)
- **Missing fields** — gaps against the 5 core fields from
`references/provider-extraction-patterns.md` (name, credentials, specialty,
contact, education)
- **Enrichment targets** — additional fields the user requested (reviews, regulatory,
accreditation)
**Early exit — no gaps:** If all providers are already High confidence (5/5 fields),
skip to Step 5 (WSA enrichment) or report: "All providers already have complete
profiles. Want me to add supplementary data (reviews, clinical trials, accreditation)
instead?"
Build a gap analysis summary:
> "Analyzing **N providers**:
> - Names: N/N present
> - Credentials: N/N present (N missing)
> - Specialty: N/N present (N missing)
> - Contact info: N/N present (N missing)
> - Education: N/N present (N missing)
>
> Starting enrichment for **N providers with gaps**..."
Run Layer 2 WSA discovery now that you know the specialty:
```bash
nimble extract:templates list --limit 50 # filter items for "[specialty]"
nimble extract:templates list --limit 50 # filter items for "[directory-user-mentioned]"
```
See `references/wsa-reference.md` for session-specific discovery.
### Step 3: Web Search for Provider Identity
For each provider with gaps, find their practice website and bio page:
```bash
nimble search --query "[provider name] [credentials] [location] [specialty]" --max-results 5 --search-depth lite
```
**Search strategy:**
- Include all known fields in the query to disambiguate common names
- Prioritize results from practice websites over directory listings
- If the provider has a known practice name, add it to the query
- For providers with only name + state, broaden: `"[name] [state] doctor"`
**Result selection:** Pick the most relevant result — practice bio page > healthcare
directory profile > LinkedIn. Save the selected URL for extraction.
For 10+ providers, use sub-agents (see Sub-Agent Strategy below).
**Checkpoint (mandatory):** You MUST write the checkpoint file before proceeding.
Interrupted runs with 20+ providers waste significant API credits without resume.
```bash
echo '{...}' > ~/.nimble/memory/healthcare-providers-enrich/checkpoints/{slug}/search.json
```
### Step 4: Extract Missing Fields
Choose extraction strategy based on provider count. Follow the Scaled Execution
pattern from `references/nimble-playbook.md` — it covers individual calls (1-10),
`extract-batch` (11-100), and the confirmation gate for larger jobs. Use the Page
Extraction with Retry pattern from the same reference for garbage detection and
retry logic.
Parse extracted content for missing fields using the detection patterns from
`references/provider-extraction-patterns.md` (credential regex, specialty keywords,
contact patterns, education mentions).
**Merge rules:**
- Only fill fields that are actually missing — never overwrite existing data
- Track which fields were added and their source URL
- If extracted data conflicts with existing data, keep the existing value and flag
the conflict for user review
**Checkpoint (mandatory):** You MUST write the checkpoint file before proceeding.
```bash
echo '{...}' > ~/.nimble/memory/healthcare-providers-enrich/checkpoints/{slug}/extraction.json
```
### Step 5: WSA Enrichment (Optional)
If the user requested reviews, regulatory data, or accreditation — or if the gap
analysis shows most core fields are already filled and enrichment adds more value:
**Run enrichment-phase WSAs** discovered in Step 0. See `references/wsa-reference.md`
for the enrichment phase mapping, agent evaluation, and fallback chains.
For each practice or provider, run relevant enrichment agents simultaneously.
Follow the Scaled Execution pattern from `references/nimble-playbook.md` for
batching.
**Merge enrichment data** into provider records:
- Reviews/ratings -> add as supplementary fields (not part of core 5)
- Clinical trial activity -> add as supplementary field
- Accreditation status -> add as supplementary field
### Step 6: Deduplication & Confidence Scoring
Follow the Entity Deduplication and Entity Confidence Scoring patterns from
`references/nimble-playbook.md`. Skill-specific dedup rules and the 5-field
confidence criteria are in `references/provider-extraction-patterns.md`.
**Enrichment-specific confidence:** Score only the **newly added** fields:
- **High** — field found and confirmed by 2+ sources
- **Medium** — field found from 1 source
- **Low** — field inferred or partially matched
### Step 7: Output
Present results as an enrichment diff — showing what was added to each provider.
Group by practice, sort by confidence within each group, and include a "What This
Means" section at the end with actionable next steps.
```markdown
# Provider Enrichment: [N] Providers Updated
*[Date] | [A] fields added across [P] providers | [H] High, [M] Medium, [L] Low confidence*
## TL;DR
Enriched [P] of [T] providers. Added [A] total fields: [breakdown by field type].
[Key finding: e.g., "Found contact info for 18 of 20 providers, 3 have clinical trials"].
## Enrichment Results
| # | Name | Added Fields | Confidence | Source |
|---|------|-------------|------------|--------|
| 1 | Dr. Jane Smith | +credentials (MD, FACS), +contact ((555) 123-4567) | High | [source](url) |
| 2 | Dr. John Doe | +specialty (General Ophthalmology), +education (Wills Eye) | Medium | [source](url) |
| 3 | Dr. Alex Chen | +contact ((555) 987-6543) | Low | [source](url) |
## Detailed Records
### Dr. Jane Smith
**Existing:** Name, State (TX)
**Added:**
- Credentials: MD, FACS — [source](url)
- Contact: (555) 123-4567 — [source](url)
- Education: Fellowship, Bascom Palmer Eye Institute — [source](url)
**Confidence:** High (3 fields added, 2 sources)
[Repeat per provider with additions]
## Providers Not Enriched
[List providers where no additional data was found, with attempted searches]
## Data Quality Summary
- **Fully enriched (5/5 fields):** [N] providers
- **Partially enriched:** [N] providers — common gaps: [list]
- **No new data found:** [N] providers
## Sources
[Clickable URL for every page used, grouped by provider]
## What This Means
[Actionable interpretation: which providers are ready to contact, which need more
data, what the enrichment coverage tells you about this list's quality]
```
**Source links are mandatory.** Every added field must trace back to a source URL.
### Step 8: Save to Memory
Make all Write calls simultaneously:
- Report -> `~/.nimble/memory/reports/healthcare-providers-enrich-{slug}-{date}.md`
- Enriched data -> `~/.nimble/memory/healthcare-providers-enrich/{slug}/enriched.json`
- Profile -> update `last_runs.healthcare-providers-enrich` in
`~/.nimble/business-profile.json` (only if profile exists)
- Follow the wiki update pattern from `references/memory-and-distribution.md`: update
`index.md` rows for all affected entity files, append a `log.md` entry for this run.
- Clean up checkpoint (complete run) or keep (partial run)
### Step 9: Share & Distribute
**Always offer distribution — do not skip.** Follow
`references/memory-and-distribution.md` for connector detection and sharing flow.
Notion: full enrichment report as a dated subpage.
Slack: TL;DR with enrichment summary and field counts only.
### Step 10: Follow-ups
- **"Tell me more about Dr. X"** -> show full enriched profile
- **"Export as CSV"** -> generate CSV with original + enriched fields
- **"Enrich more fields"** -> re-run with expanded field targets
- **"Which providers still have gaps?"** -> filter to incomplete records
**Sibling skill suggestions:**
> **Next steps:**
> - Run `healthcare-providers-verify` to validate the enriched credentials and
> license status
> - Run `healthcare-providers-extract` to discover more providers from practice
> websites
> - Run `market-finder` to find additional practices in this area
---
## Sub-Agent Strategy
For batch enrichment (10+ providers), use `nimble-researcher` agents
(`agents/nimble-researcher.md`) to parallelize search and extraction.
Follow the sub-agent spawning rules from `references/nimble-playbook.md`
(bypassPermissions, batch max 4, explicit Bash instruction, fallback on failure).
**Spawn pattern:** One agent per batch of 5 providers. Each agent runs Steps 3-4
for its assigned providers and returns enriched records. Tell each agent to use
`nimble extract-batch` for its assigned URLs rather than individual `nimble extract`
calls — one batch call per agent is faster and more reliable than sequential calls.
**Small batch optimization:** If fewer than 10 providers, run directly from the
main context instead of spawning agents.
**Fallback:** If any agent fails, run those enrichments directly from the main
context. Never leave gaps in the output.
---
## Error Handling
See `references/nimble-playbook.md` for the standard error table (missing API key,
429, 401, empty results, extraction garbage). Skill-specific errors:
- **No search results for provider:** "Couldn't find a web presence for [name] in
[state]. The name may be too common or the provider may not have an online
presence. Want me to try with additional context (practice name, specialty)?"
- **Ambiguous provider match:** "Found multiple providers named [name] in [state].
Can you confirm which one? [list top 3 with practice names]"
- **All extractions returned garbage:** "The provider websites appear to be heavily
JavaScript-rendered. Retrying with browser rendering..." (auto-retry with
`--render` per the shared pattern)
- **CSV/Sheet parse error:** "Couldn't parse the input file. Expected columns with
provider names and at least one identifier (state, specialty, or practice).
Can you paste the data directly instead?"
- **No gaps detected:** Handled in Step 2 (early exit to WSA enrichment or report).