Install
$ agentstack add skill-keinsaasforever-gtm-pipeline-skills-gtm-people-search ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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
| Input | Required | Source | |-------|----------|--------| | Company list (CSV with names + domains) | For company mode | Company Search / Company Enrichment output, or user-provided | | Target roles / job titles | Yes | User prompt or ICP definition | | Location filter | Recommended | User prompt or ICP definition | | Search mode | Yes | company (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:
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)
Default flow: BetterContact first → FullEnrich fallback if BC returns 0 contacts. For DACH SMEs, add a Pipe0 Amplemarket pass on whatever FE missed — different index, additive coverage.
| Priority | Provider | Cost | LinkedIn URLs | Key Strength | |----------|----------|------|---------------|-------------| | 1st | BetterContact Lead Finder | 0.10 cr/request | Yes | Cheaper fixed cost; good for high-volume batches | | 2nd | FullEnrich Finder | 0.25 cr/person | Yes | Richest filters; richer fields (seniority, headcount, industry) | | 3rd | Pipe0 Amplemarket | 3.00 cr/page (≤100 results) | Yes | Cheap waterfall for FE/BC misses — separate index covers different shops (+36% shop coverage on DACH SMEs where FE had 0 hits). Supports company domain + name filters | | 4th | Pipe0 Crustdata | 5.00 cr/page | Yes | Final fallback — low additive value when run after Amplemarket (+1 shop out of 81 in DACH test) | | 5th | PhantomBuster Company Employees Export | Free (LinkedIn account) | Yes | Good when SN not available; scrapes directly from LinkedIn company page | | 6th | PhantomBuster SN Search Export | Free (SN account) | Yes | Full SN search power; use when you have a saved SN search URL |
Indexes barely overlap on small EU SMEs — running FE → Amplemarket → Crustdata 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. Always run cheapest provider first.
Two-tier search pattern (proven on EU e-commerce campaigns):
- Tier 1 — E-commerce + Marketing titles (always run)
- 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 for both BC (primary) and FE (fallback). 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)
| Priority | Provider | Cost | Key Strength | |----------|----------|------|-------------| | 1st | Parallel FindAll | varies by processor | Discover people matching criteria from web sources | | 2nd | BetterContact Search | TBD | Search without company filter | | 3rd | Pipe0 Amplemarket | 3.00 cr/page | Structured filters (location, industry, employer revenue, founded year, departments, seniority) | | 4th | Pipe0 Crustdata | 5.00 cr/page | Richest persona filters (experience, seniority, skills, education, certifications, career movement) |
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_locationfilter for.comdomains) - 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
sourcecolumn - 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
Request
{
"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
| Filter | Type | Example | |--------|------|---------| | current_company_names | object[] | {"value": "Anthropic", "exact_match": true} | | current_company_domains | object[] | {"value": "google.com", "exact_match": true} | | current_company_linkedin_urls | object[] | LinkedIn company URL | | current_company_industries | object[] | "Software Development" | | current_company_types | object[] | "Public Company", "Privately Held" | | current_company_headquarters | object[] | "San Francisco" | | current_company_headcounts | object[] | {"min": 50, "max": 200} | | current_company_founded_years | object[] | {"min": 2020, "max": 2024} | | current_position_titles | object[] | "Chief Technology Officer" | | current_position_seniority_level | object[] | "Director", "VP", "C-level" | | past_position_titles | object[] | Past job title | | past_company_names / domains | object[] | Previous employer | | person_names | object[] | "John Smith" | | person_linkedin_urls | object[] | Direct LinkedIn URL lookup | | person_locations | object[] | "South Africa", "California" | | person_skills | object[] | "JavaScript", "Project Management" | | current_position_years_in | object[] | {"min": 0, "max": 1} (new in role) | | current_company_years_at | object[] | {"min": 2, "max": 5} (tenure) | | person_universities | object[] | "Stanford University" | | current_company_days_since_last_job_change | object[] | {"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.
people = response.get("people", [])
for person in people:
name = person.get("full_name", "")
title = person.get("employment", {}).get("current", {}).get("title", "")
li_url = (person.get("social_profiles", {}) or {}).get("linkedin", {}).get("url", "")
loc_obj = person.get("location", {})
location = f"{loc_obj.get('city', '')}, {loc_obj.get('country', '')}".strip(", ")
# company info also nested: person["employment"]["current"]["company"]["name"]
Key Notes
current_company_linkedin_urlsfilter accepts LinkedIn company URLs directly (e.g.https://www.linkedin.com/company/dojo-tech/) — no domain lookup needed when you have LinkedIn company URLs in your input CSV- Hit rate for EU SME audience (via LinkedIn URL filter): ~48% (15/31 companies). Very small/niche companies with few employees often have low FE index coverage — expect 0 results.
Cost
0.25 credits per person returned.
Docs
https://docs.fullenrich.com/api/v2/people/search/post
Provider B: BetterContact Lead Finder
Endpoint: POST https://app.bettercontact.rocks/api/v2/lead_finder/async Auth: X-API-Key: $BETTERCONTACT_API_KEY
Submit
{
"filters": {
"company": {
"include": ["virginactive.co.za"]
},
"lead_location": {
"include": ["South Africa"]
},
"lead_job_title": {
"include": [
"marketing manager", "head of marketing", "brand manager",
"marketing director", "CMO", "digital marketing"
],
"exact_match": false
}
},
"max_leads": 10
}
Returns: { "success": true, "request_id": "abc123" }
Poll
GET https://app.bettercontact.rocks/api/v2/lead_finder/async/{request_id}
Done when: response["status"] == "terminated" (not "completed") Typical wait: 30–60 seconds per request.
Parse
leads = response.get("leads", [])
for lead in leads:
name = lead.get("contact_full_name", "")
title = lead.get("contact_job_title", "")
li_url = lead.get("contact_linkedin_profile_url", "")
company = lead.get("company_name", "")
domain = lead.get("company_domain", "")
country = lead.get("contact_location_country", "")
co_li = lead.get("company_linkedin_url", "")
Global Domain Contamination
When using a global domain (.com for Samsung, H&M, Amazon, etc.), BetterContact returns employees from all countries. Always add "lead_location": {"include": ["Target Country"]} for global-domain companies. Local ccTLD domains (.co.za, .de) are safe without it.
Cost
0.10 credits per request (fixed, regardless of number of leads returned or zero results).
Fields Returned
BC returns fewer fields than FE — no seniority, companyHeadcount, companyIndustry, or roleStartDate. Derive linkedinProfileSlug from the LinkedIn URL (/in/). Split contact_full_name into first/last manually.
Filter Compatibility
When filtering contacts by job title keyword, BC uses contact_job_title (not FE's nested employment.current.title). Ensure your filter function checks both:
title = (p.get("current_position_title") or
p.get("contact_job_title") or # BC format
p.get("employment", {}).get("current", {}).get("title") or "")
Hit Rate
- SA marketing roles, local (.co.za): ~65–70%
- DACH SME (March 2026): 1/3 companies (33%) — BC missed 2/3; FE found contacts at 2/3 on same set
- EU furniture e-commerce SME (April 2026): ~15–20% overall; T2 leadership tier recovers ~10% more
When BC Finds Nothing
Fall back to FullEnrich. BC has lower index coverage for small EU SMEs, especially IT/PL markets. FE's richer filters and larger index often surface contacts BC misses — and vice versa. Running both covers ~25–30% of shops vs ~20% with either alone.
Docs
https://doc.bettercontact.rocks/api-reference/endpoint/leadfinderpost
Provider C: Parallel FindAll (Persona Mode)
Endpoint: POST https://api.parallel.ai/v1beta/findall/runs Required header: parallel-beta: findall-2025-09-15 Auth: x-api-key: $PARALLEL_API_KEY
Always ask which processor to use: core, core2x, pro, ultra
Request
{
"objective": "Find heads of marketing at e-commerce companies in South Africa with 50-500 employees",
"entity_type": "people",
"match_conditions": [
{"name": "role", "description": "Person must hold a marketing leadership role (Head of Marketing, CMO, Marketing Director)"},
{"name": "location", "description": "Person must be based in South Africa"},
{"name": "company_size", "description": "Company must have roughly 50-500 employees"}
],
"generator": "core",
"match_limit": 25
}
Writing good objectives:
- Write like a research brief — detailed, with source guidance
- Describe what signals/sources to start from, not just what to find
- Include geographic, industry, and size constraints in the objective text AND as match_conditions
Poll
GET /v1beta/findall/runs/{findall_id} # status
GET /v1beta/findall/runs/{findall_id}/result # results when complete
Timeout: 15 min for core/core2x, 30 min for pro/ultra
Enrich FindAll Results
After FindAll, add structured fields:
POST /v1beta/findall/runs/{findall_id}/enrich
Always include company_website and linkedin_company_url in output schema.
Rules
- Always ask which processor to use — never decide without asking
- Never re-run before reviewing results
- Present the full request payload for review before executing
- Assess accuracy using confidence score and reasoning
Docs
Always check latest docs via context7 (libraryName: parallel-web) before building — endpoints and parameters evolve.
Provider E: Pipe0 Amplemarket (Filter-Based)
Endpoint: POST https://api.pipe0.com/v1/search/run/sync (singular search) Search ID: people:profiles:amplemarket@1 Auth: Authorization: Bearer $PIPE0_API_KEY Use curl — Python requests blocked by Cloudflare. Best for: Company-mode fallback when FE/BC miss (cheap at 3 cr/page) AND persona-mode filter search.
Request (Company Mode)
{
"config": {"environment": "production", "dedup": {"strategy": "default"}},
"search": {
"search_id": "people:profiles:amplemarket@1",
"config": {
"limit": 5,
"filters": {
"current_employer_website_urls": {"include": ["example.com"]},
"current_employer_names": {"include": ["Example GmbH"]},
"current_job_titles": ["CEO", "Geschäftsführer", "Founder", "Head of Marketing"],
"current_locations": {"include": ["Germany"]}
}
}
}
}
⚠ Filter Format Gotcha (Amplemarket-specific)
current_job_titlesmust be a PLAIN ARRAY (["CEO", "Founder"]), NOT{"include": [...]}like the other fields. Crustdata uses the object form. Getting it wrong → 422 validation error.- Most other filters use the
{"include": [...], "exclude": [...]}object form.
Available Filters (discovered via sandbox probe)
person_names, school_names,
current_locations, current_job_titles, current_departments,
current_job_functions, current_seniority_levels,
current_employer_names, current_employer_website_urls,
current_employer_linkedin_industries, current_employer_locations,
current_employer_investors, current_employer_founded_year,
current_employer_estimated_revenue, current_employer_open_positions_titles
Response
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [keinsaasforever](https://github.com/keinsaasforever)
- **Source:** [keinsaasforever/gtm-pipeline-skills](https://github.com/keinsaasforever/gtm-pipeline-skills)
- **License:** MIT
- **Homepage:** https://www.keinsaas.com/de/research-agent
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.