keinsaasforever/gtm-pipeline-skills/gtm-people-search

Claude Code and agent skill by keinsaasforever/gtm-pipeline-skills

Trust tier 2
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Find contacts at specific companies or by persona. Use when you have a company list and need contacts with specific roles (company mode), or when building a persona-based prospect list without a company list (persona mode). Also triggers on "find contacts", "search for people at [company]", "find [role] contacts", "people search".

Install

npx vskill@latest install keinsaasforever/gtm-pipeline-skills/gtm-people-search

SKILL.md

People Search

Find contacts at specific companies or by persona. Returns a CSV with LinkedIn profile URLs, ready for enrichment.

Read `~/.claude/skills/gtm-pipeline/_shared/conventions.md` before executing.


When to Use

  • You have a company list and need contacts with specific roles
  • You need to build a persona-based prospect list (no company list)
  • A demo was triggered and you need ~10 contacts matching a prompt

Inputs

InputRequiredSource
Company list (CSV with names + domains)For company modeCompany Search / Company Enrichment output, or user-provided
Target roles / job titlesYesUser prompt or ICP definition
Location filterRecommendedUser prompt or ICP definition
Search modeYescompany (have company list) or persona (no company list)

If company domains are missing, use SerpAPI domain lookup first (see Step 0 below).


Step 0: Domain Lookup (SerpAPI) — When Domains Are Missing

Most people-search APIs require domains, not LinkedIn URLs.

GET https://serpapi.com/search
?engine=google_light
&q={company_name}
&location={target_country}
&google_domain={country_google_domain}
&api_key=YOUR_KEY

Extract domain:

python
import urllib.parse
organic = response.get("organic_results", [])
link = organic[0].get("link", "")
domain = urllib.parse.urlparse(link).netloc.lstrip("www.")

Spot-check all domains before using — SerpAPI returns wrong results for generic names and global brands.

Env var: SERPAPI_API_KEY


Provider Selection

Company mode (have company list)

Finder cadence (segment-routed waterfall): FullEnrich Finder → BetterContact Lead Finder → Pipe0 searches (`amplemarket@2` → `crustdata@3` → `parallel@1`, last resort). Max 2 attempts per source (e.g. a title-token query then a name-only query), then fall through. Lead with FullEnrich for SME / owner-led / non-English-market segments (better indexed there); lead with BetterContact for broader / English-market / larger companies. Add a Pipe0 pass on whatever the finders missed — different index, additive coverage. See conventions "People-Source Cadence" for the full waterfall and fallback rule; the table below is per-provider mechanics and cost, not a fixed running order.

PriorityProviderCostLinkedIn URLsKey Strength
1stBetterContact Lead Finder0.10 cr/requestYesCheaper fixed cost; good for high-volume batches
2ndFullEnrich Finder0.25 cr/personYesRichest filters; richer fields (seniority, headcount, industry)
3rdPipe0 `amplemarket@2`3.00 cr/page, flat (1 page = 100 records)YesCheap waterfall for FE/BC misses — separate index covers different shops (+36% shop coverage on DACH SMEs where FE had 0 hits). Company domain + name filters. Always ask for limit: 100 — 25 costs the same
4thPipe0 `crustdata@3`0.15 cr per result returned (limit: 25 → 3.75 worst case)YesRichest filter set (29). Low additive value when run after amplemarket (+1 shop out of 81 in DACH test)
5thPipe0 `parallel@1`0.50 cr flatYesAI entity search — no filters at all, every criterion goes in the objective prose. Use when the structured searches return 0
6thPhantomBuster Company Employees ExportFree (LinkedIn account)YesGood when SN not available; scrapes directly from LinkedIn company page
7thPhantomBuster SN Search ExportFree (SN account)YesFull SN search power; use when you have a saved SN search URL

Indexes barely overlap on small EU SMEs — running FE → amplemarket@2 → crustdata@3 is additive, not redundant. In an April 2026 DACH e-commerce run (81 FE-missed shops): amplemarket recovered 29 shops (74 contacts, 35%), crustdata added 1 more shop as fallback. Amplemarket stays first: 3.00 flat for up to 100 rows beats crustdata's 3.75 worst case for 25.

Two-tier search pattern (proven on EU e-commerce campaigns):

  1. Tier 1 — E-commerce + Marketing titles (always run)
  2. Tier 2 — Leadership titles (CEO/MD/Founder) — only if Tier 1 returns 0 contacts AND company traffic ≤ 200K visits/month (large shops likely have dedicated e-comm/marketing staff indexed by FE)

Run the same two-tier logic on whichever finder leads the cadence for the segment (FE or BC). BC cost is per-request regardless of results; FE cost is per-person returned.

When PhantomBuster is selected: read _shared/phantombuster.md and use the /phantombuster skill to generate the script. Phantom scripts: "LinkedIn Company Employees Export" (config key PB_AGENT_EMPLOYEES) and "Sales Navigator Search Export" (config key PB_AGENT_SN_SEARCH).

Persona mode (no company list)
PriorityProviderCostKey Strength
1stParallel FindAllvaries by processorDiscover people matching criteria from web sources
2ndBetterContact SearchTBDSearch without company filter
3rdPipe0 `crustdata@3`0.15 cr/resultRichest persona filters (29): headcount brackets, industries, seniority, tenure, skills, education, career movement
4thPipe0 `amplemarket@2`3.00 cr/pageStructured filters (21): location, headcount, LinkedIn industry, employer revenue, founded year, departments, seniority, funding, follower level, b2b/b2c
5thPipe0 `parallel@1`0.50 crProse criteria that no filter expresses (keywords, signals, free-text ICP brief)

Persona mode has no company anchor, so an unfiltered search is a fully-billed market-wide dump. Never send a persona search without both a title list and at least one resolved region — the repo's builder raises on either being empty (see Provider E).

Do NOT use Parallel Task enrichment for people — model guesses titles, returns wrong roles. Use FindAll.

Manual / one-time

Both BetterContact and FullEnrich dashboards offer free search without API credits.


Execution Protocol

1. Sandbox / Docs Check
  • Verify request/response structure with 1 record, zero cost
  • For Pipe0: use "environment": "sandbox"
  • For FE/BC: review docs or test via dashboard
2. Test Batch (15 companies or 15 records)
  • Run chosen provider on 15 companies in production
  • Print and review every row: company names, contact names, job titles, locations, LinkedIn URLs
  • Check for global domain contamination (BC: add lead_location filter for .com domains)
  • Assess hit rate, data completeness, relevance
3. Review with User
  • Present test results with hit rate and sample rows
  • Flag issues (wrong country, irrelevant titles, missing LinkedIn URLs)
  • Suggest improvements or provider switch if results are poor
  • Get approval before full run
4. Full Run
  • Submit remaining companies (skip test batch)
  • Save request IDs to file immediately (recovery if crash)
  • Poll with sufficient timeout
  • Save results incrementally to CSV after each batch
5. Consolidation
  • Merge test + full run results
  • Deduplicate by LinkedIn URL (fallback: name + company)
  • Clean names/titles (.title(), strip whitespace)
  • Add source column
  • Write to csv/intermediate/contacts_found.csv

Provider A: FullEnrich Finder

Endpoint: POST https://app.fullenrich.com/api/v2/people/search Auth: Authorization: Bearer $FULLENRICH_API_KEY Cloudflare: call with curl + a browser User-Agent — Python requests/urllib get blocked (error 1010). Never ship a urllib-based provider script. See conventions rule #8. Domain-keyed index: the Finder is keyed on company domain and misses orgs whose domain differs from the indexed one — prefer name-based search (current_company_names + location) over the domain filter; validate hits by domain-root or name token.

Request
json
{
"offset": 0,
"limit": 100,
"current_company_domains": [
{"value": "example.com", "exact_match": true, "exclude": false}
],
"current_position_titles": [
{"value": "Marketing Manager", "exact_match": false, "exclude": false},
{"value": "Head of Marketing", "exact_match": false, "exclude": false}
],
"person_locations": [
{"value": "South Africa", "exact_match": false, "exclude": false}
]
}
Available Filters
FilterTypeExample
current_company_namesobject[]{"value": "Anthropic", "exact_match": true}
current_company_domainsobject[]{"value": "google.com", "exact_match": true}
current_company_linkedin_urlsobject[]LinkedIn company URL
current_company_industriesobject[]"Software Development"
current_company_typesobject[]"Public Company", "Privately Held"
current_company_headquartersobject[]"San Francisco"
current_company_headcountsobject[]{"min": 50, "max": 200}
current_company_founded_yearsobject[]{"min": 2020, "max": 2024}
current_position_titlesobject[]"Chief Technology Officer"
current_position_seniority_levelobject[]"Director", "VP", "C-level"
past_position_titlesobject[]Past job title
past_company_names / domainsobject[]Previous employer
person_namesobject[]"John Smith"
person_linkedin_urlsobject[]Direct LinkedIn URL lookup
person_locationsobject[]"South Africa", "California"
person_skillsobject[]"JavaScript", "Project Management"
current_position_years_inobject[]{"min": 0, "max": 1} (new in role)
current_company_years_atobject[]{"min": 2, "max": 5} (tenure)
person_universitiesobject[]"Stanford University"
current_company_days_since_last_job_changeobject[]{"min": 0, "max": 90} (recent hires)

All filters support exclude: true for negative matching. Multiple filters within same field = AND logic.

Pagination: offset + limit (max 100/page, max offset 10,000). Beyond 10k: use search_after cursor.

Response

Response fields are nested — current_position_title and linkedin_url do NOT exist at top level.

⚠ LinkedIn URL path gotcha: the URL lives under social_profiles.professional_network.url, NOT social_profiles.linkedin.url. Older notes/snippets used .linkedin.url and silently returned empty strings for every row. Always parse professional_network first.

Also: the seniority field is seniority (not seniority_level) at employment.current.seniority. Values include Manager, Senior, Head, Director, VP, C-Level.

python
people = response.get("people", [])
for person in people:
name = person.get("full_name", "")
current = (person.get("employment", {}) or {}).get("current", {}) or {}
title = current.get("title", "")
seniority = current.get("seniority", "")
social = person.get("social_profiles", {}) or {}
li_url = (social.get("professional_network", {}) or {}).get("url", "")
loc_obj = person.get("location", {}) or {}
location = f"{loc_obj.get('city', '')}, {loc_obj.get('country', '')}".strip(", ")
# company info also nested: current["company"]["name"], current["company"]["domain"]

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
62
Repository forks
6
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/keinsaasforever/gtm-pipeline-skills/gtm-people-search/badge)](https://verified-skill.com/skills/keinsaasforever/gtm-pipeline-skills/gtm-people-search)

Something wrong with this skill or its page? Tell us.