Lead Prospecting
ICP → companies → the right person at each, as a CSV of leads (ICP-matched contacts,
not qualified prospects). Email lookup and deliverability are the companion
email-verification skill, handed off after Step 3. Never drafts or sends outreach.
Prerequisites
taskfuel CLI connected (taskfuel whoami), balance ≥ ~$0.35 for a full discover run.
- Base
taskfuel skill covers payment mechanics — quote with --dry-run, pay with
--max-amount, never suppress stderr on a paid call. Read it first if you haven't.
Rules
- Prompt, never guess. Ask for each step's parameters with examples so answering takes
one word. Guessed ICPs and job functions return wrong results or none.
- Announce cost, then wait for OK. Never chain straight into a paid call. Confirm before
any step adds >~50 rows.
- Seller-agnostic — no built-in ICP. Fit criteria come only from the user's own words;
never assume what is being sold.
Intake
- Entry point: discover (Step 1), or bring-your-own domains — skip Step 1, seed the
table from the pasted domains, start at Step 2.
- In discover mode, pick the route from the shape of the ICP — never ask the user to name
a tool:
- Categorical / numeric (an industry, a country, a headcount band) → route A.
- Thematic / semantic ("customer-support automation", "cross-border payroll") → route B.
FullEnrich's 490-value industry enum has no agent/AI entry — its documented stand-ins
(
Software Development for SaaS, Research Services for AI labs) are far wider than the theme.
- Both kinds given → route B to find them, then filter on the numbers client-side.
- Fit criteria (optional): "What are you selling, and what makes a company a good fit?"
Free text. Blank ⇒ skip Step 1b and present unranked.
Deliverable
leads-YYYY-MM-DD.csv, written at the first data step and enriched in place. Nothing
discarded — one row per person, plus a placeholder row per contactless company. Selected?
marks the primary contact; the rest stay as fallbacks. All rows go to the CSV; show only a
top slice in chat.
Company | Domain | Fit | Fit reason | Person | Title | Seniority | LinkedIn URL | Selected? | Status
email-verification appends Email | Email source | Verified? | Score on handoff, and keeps
writing the existing Status column rather than adding one — leave the four new columns
absent until then rather than writing them empty.
Status: company-found → duplicate | no-contact-found | candidate → selected.
The companion skill continues this same enum from selected, with email-found |
email-constructed → verified | email-unconfirmed | verify-failed.
1 — discover companies · skipped in BYO-domains mode
Intake picked the route. Parameters, response shape and the gotchas that cost real money live
beside this file — read the one route you picked before calling it:
- Route A · categorical/numeric ICP · $0.15/call, free when zero results →
ROUTE-A.md
- Route B · thematic ICP · $0.01 flat →
ROUTE-B.md
→ Company, Domain, HQ, headcount · company-found
1a — dedupe · free
- Same non-empty domain → keep the fullest, mark copies
duplicate (park, don't delete).
- Cross-domain near-dupes (same name/HQ, different TLD) → ask which to keep; never
auto-merge, since TLDs can be distinct entities.
- Empty domain → resolve by web search before Step 2.
- After route B: drop any platform host
excludeDomains did not catch — app stores,
review sites, link-in-bio pages. Unfiltered, these were 16 of 31 rows in testing (9 of them
*.notion.site), so add each new offender to excludeDomains for the next call.
1b — score fit · free · only if fit criteria given
- The scoring text is already in the Step 1 response — no extra call. After route A use
description + specialties; after route B use results[].entities[].properties.description
plus the result text.
- To target the criteria directly, re-run route B with
contents.highlights.query set to the
fit criteria — it takes its own query and returns a per-company relevance snippet, still
$0.01.
- Set Fit (
High/Med/Low) and a one-line Fit reason citing the criteria.
- Annotate, don't gate — low-fit rows stay in the table; it is a heuristic over marketing copy.
- → Fit, Fit reason
2 — people-search · POST https://stableenrich.dev/api/fullenrich/people-search · $0.15/call, free when no matches
Ask: job function and/or seniority, and which domains.
- Batch every domain into one
current_company_domains — same $0.15 as a single company.
One-at-a-time multiplies cost by company count for no extra data.
current_position_job_functions takes canonical Function enums (Software =
engineering, Sales, Product, Design, Marketing, Human Resources, Operations,
Finance, Legal, Executive & Leadership…). Full list:
taskfuel discover POST https://stableenrich.dev/api/fullenrich/people-search.
current_position_titles is exact-match only — "CTO" matches that literal string and
returns far fewer rows. Prefer seniority or function.
current_position_seniority_level: Owner, Founder, C-level, Partner, VP, Head,
Director, Manager, Senior.
- AND across categories, OR within one — use ≤2 categories. Three or more often returns
zero. Prefer domains plus one of seniority/function.
- Check
metadata.total — a free early warning of pool size. Page with offset/search_after
at another $0.15 each.
- Response is a lean roster:
people[].employment.current.{title,seniority}, LinkedIn
under people[].social_profiles.professional_network, company detail in a top-level
companies map keyed by company_id, plus headline, location and educations. Add
include_employment_history or verbose only if full career history is needed.
- Expect most domains to return nobody — 19 domains yielded 9 people at 6 companies. A thin
roster is normal for small companies, not a failed call: mark the rest
no-contact-found.
- When FullEnrich is unavailable, or the run has ≲15 companies, Exa substitutes at
$0.01/company →
PEOPLE-EXA.md.
- → Person, Title, Seniority, LinkedIn URL, country ·
candidate | no-contact-found
3 — select · free
- Ask run mode first: single (one picked company, cheapest) or fan-out (one contact
per company; handoff cost scales at ~$0.021 each). All rows stay in the table either way —
mode only sets how many advance.
- Then pick manually, or auto-select most senior: Owner/Founder/C-level > VP > Head >
Director > Manager. In fan-out, ask the rule once, apply it to all, and show the planned
table for a single approval.
- → Selected?=yes ·
selected
Handoff to email-verification · ~$0.021 per contact
Finding and verifying the address is a separate skill. Invoke it with the selected rows; it
enriches this same CSV in place. Per contact:
{"first_name": "Alex", "last_name": "Moreau", "domain": "example.com"}
returning {"email", "email_source", "verified", "score", "status"}.
- Pass names exactly as they appear — never normalize diacritics on the way out.
Transliteration is language-dependent and belongs to the finder; a fixed
ü→u rule produced
a confirmed-undeliverable address in testing.
- Hand off full surnames. Recover a truncated one from the LinkedIn handle, or select a
different contact — a bare initial cannot be looked up.
- State the cost before handing off: selected rows × ~$0.021.
1---2name: lead-prospecting3description: Build a CRM-ready B2B lead list through taskfuel.ai — discover companies matching an ideal-customer profile, then find the right decision-maker at each, paid per call from the prepaid balance. Use when the user wants sales leads, a target account list, or contacts at companies they name.4license: MIT5---67# Lead Prospecting89ICP → companies → the right person at each, as a CSV of **leads** (ICP-matched contacts,10not qualified prospects). Email lookup and deliverability are the companion11`email-verification` skill, handed off after Step 3. **Never drafts or sends outreach.**1213## Prerequisites1415- `taskfuel` CLI connected (`taskfuel whoami`), balance ≥ ~$0.35 for a full discover run.16- Base `taskfuel` skill covers payment mechanics — quote with `--dry-run`, pay with17 `--max-amount`, never suppress stderr on a paid call. Read it first if you haven't.1819## Rules20211. **Prompt, never guess.** Ask for each step's parameters with examples so answering takes22 one word. Guessed ICPs and job functions return wrong results or none.232. **Announce cost, then wait for OK.** Never chain straight into a paid call. Confirm before24 any step adds >~50 rows.253. **Seller-agnostic — no built-in ICP.** Fit criteria come only from the user's own words;26 never assume what is being sold.2728## Intake2930- **Entry point:** *discover* (Step 1), or *bring-your-own domains* — skip Step 1, seed the31 table from the pasted domains, start at Step 2.32- **In discover mode, pick the route from the shape of the ICP — never ask the user to name33 a tool:**34 - **Categorical / numeric** (an industry, a country, a headcount band) → **route A**.35 - **Thematic / semantic** ("customer-support automation", "cross-border payroll") → **route B**.36 FullEnrich's 490-value industry enum has no agent/AI entry — its documented stand-ins37 (`Software Development` for SaaS, `Research Services` for AI labs) are far wider than the theme.38 - Both kinds given → route B to find them, then filter on the numbers client-side.39- **Fit criteria (optional):** *"What are you selling, and what makes a company a good fit?"*40 Free text. Blank ⇒ skip Step 1b and present unranked.4142## Deliverable4344`leads-YYYY-MM-DD.csv`, written at the first data step and enriched in place. **Nothing45discarded** — one row per person, plus a placeholder row per contactless company. `Selected?`46marks the primary contact; the rest stay as fallbacks. All rows go to the CSV; show only a47top slice in chat.4849`Company | Domain | Fit | Fit reason | Person | Title | Seniority | LinkedIn URL | Selected? | Status`5051`email-verification` appends `Email | Email source | Verified? | Score` on handoff, and keeps52writing the existing `Status` column rather than adding one — leave the four new columns53absent until then rather than writing them empty.5455`Status`: `company-found` → `duplicate` | `no-contact-found` | `candidate` → `selected`.56The companion skill continues this same enum from `selected`, with `email-found` |57`email-constructed` → `verified` | `email-unconfirmed` | `verify-failed`.5859## 1 — discover companies · *skipped in BYO-domains mode*6061Intake picked the route. Parameters, response shape and the gotchas that cost real money live62beside this file — **read the one route you picked before calling it**:6364- **Route A** · categorical/numeric ICP · $0.15/call, free when zero results →65 [`ROUTE-A.md`](ROUTE-A.md)66- **Route B** · thematic ICP · $0.01 flat → [`ROUTE-B.md`](ROUTE-B.md)6768→ Company, Domain, HQ, headcount · `company-found`6970## 1a — dedupe · free7172- Same non-empty domain → keep the fullest, mark copies `duplicate` (park, don't delete).73- Cross-domain near-dupes (same name/HQ, different TLD) → **ask which to keep**; never74 auto-merge, since TLDs can be distinct entities.75- Empty domain → resolve by web search before Step 2.76- **After route B:** drop any platform host `excludeDomains` did not catch — app stores,77 review sites, link-in-bio pages. Unfiltered, these were 16 of 31 rows in testing (9 of them78 `*.notion.site`), so add each new offender to `excludeDomains` for the next call.7980## 1b — score fit · free · *only if fit criteria given*8182- The scoring text is already in the Step 1 response — **no extra call.** After route A use83 `description` + `specialties`; after route B use `results[].entities[].properties.description`84 plus the result `text`.85- To target the criteria directly, re-run route B with `contents.highlights.query` set to the86 fit criteria — it takes its **own** query and returns a per-company relevance snippet, still87 $0.01.88- Set **Fit** (`High`/`Med`/`Low`) and a one-line **Fit reason** citing the criteria.89- **Annotate, don't gate** — low-fit rows stay in the table; it is a heuristic over marketing copy.90- → Fit, Fit reason9192## 2 — people-search · `POST https://stableenrich.dev/api/fullenrich/people-search` · $0.15/call, free when no matches9394**Ask:** job function and/or seniority, and which domains.9596- **Batch every domain into one `current_company_domains`** — same $0.15 as a single company.97 One-at-a-time multiplies cost by company count for no extra data.98- `current_position_job_functions` takes **canonical Function enums** (`Software` =99 engineering, `Sales`, `Product`, `Design`, `Marketing`, `Human Resources`, `Operations`,100 `Finance`, `Legal`, `Executive & Leadership`…). Full list:101 `taskfuel discover POST https://stableenrich.dev/api/fullenrich/people-search`.102- `current_position_titles` is **exact-match only** — "CTO" matches that literal string and103 returns far fewer rows. Prefer seniority or function.104- `current_position_seniority_level`: `Owner`, `Founder`, `C-level`, `Partner`, `VP`, `Head`,105 `Director`, `Manager`, `Senior`.106- **AND across categories, OR within one — use ≤2 categories.** Three or more often returns107 zero. Prefer domains plus one of seniority/function.108- Check `metadata.total` — a free early warning of pool size. Page with `offset`/`search_after`109 at another $0.15 each.110- Response is a **lean roster**: `people[].employment.current.{title,seniority}`, LinkedIn111 under `people[].social_profiles.professional_network`, company detail in a top-level112 `companies` map keyed by `company_id`, plus `headline`, `location` and `educations`. Add113 `include_employment_history` or `verbose` only if full career history is needed.114- **Expect most domains to return nobody** — 19 domains yielded 9 people at 6 companies. A thin115 roster is normal for small companies, not a failed call: mark the rest `no-contact-found`.116- **When FullEnrich is unavailable, or the run has ≲15 companies**, Exa substitutes at117 $0.01/company → [`PEOPLE-EXA.md`](PEOPLE-EXA.md).118- → Person, Title, Seniority, LinkedIn URL, country · `candidate` | `no-contact-found`119120## 3 — select · free121122- **Ask run mode first:** *single* (one picked company, cheapest) or *fan-out* (one contact123 per company; handoff cost scales at ~$0.021 each). All rows stay in the table either way —124 mode only sets how many advance.125- Then pick manually, or auto-select **most senior**: Owner/Founder/C-level > VP > Head >126 Director > Manager. In fan-out, ask the rule **once**, apply it to all, and show the planned127 table for a **single approval**.128- → Selected?=yes · `selected`129130## Handoff to `email-verification` · ~$0.021 per contact131132Finding and verifying the address is a separate skill. Invoke it with the selected rows; it133enriches this same CSV in place. Per contact:134135```json136{"first_name": "Alex", "last_name": "Moreau", "domain": "example.com"}137```138returning `{"email", "email_source", "verified", "score", "status"}`.139140- **Pass names exactly as they appear — never normalize diacritics on the way out.**141 Transliteration is language-dependent and belongs to the finder; a fixed `ü→u` rule produced142 a confirmed-undeliverable address in testing.143- **Hand off full surnames.** Recover a truncated one from the LinkedIn handle, or select a144 different contact — a bare initial cannot be looked up.145- State the cost before handing off: selected rows × ~$0.021.