agentsclimarketplace

Gtm people search

Skill vivekkhimani/gtm-tools-template/.agents/skills/gtm-people-search

Agent-first GTM operating manual template: positioning, lead pipeline, outreach, playbooks, investor materials + 22 agent skills

Install
npx -y skills add vivekkhimani/gtm-tools-template --skill gtm-people-search

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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".

SKILL.md

22.1 KB, as published. Nobody here has run it

People Search

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

Read .agents/skills/_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:

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.

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 Amplemarket3.00 cr/page (≤100 results)YesCheap 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
4thPipe0 Crustdata5.00 cr/pageYesFinal fallback — low additive value when run after Amplemarket (+1 shop out of 81 in DACH test)
5thPhantomBuster Company Employees ExportFree (LinkedIn account)YesGood when SN not available; scrapes directly from LinkedIn company page
6thPhantomBuster 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 → 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):

  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 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)

PriorityProviderCostKey Strength
1stParallel FindAllvaries by processorDiscover people matching criteria from web sources
2ndBetterContact SearchTBDSearch without company filter
3rdPipe0 Amplemarket3.00 cr/pageStructured filters (location, industry, employer revenue, founded year, departments, seniority)
4thPipe0 Crustdata5.00 cr/pageRichest 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_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

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

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 nestedcurrent_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_urls filter 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/<slug>). 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/lead_finder_post


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_titles must 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

results = response.get("results", [])
for r in results:
    name    = r.get("name", {}).get("value", "")
    title   = r.get("job_title", {}).get("value", "")
    li_url  = r.get("profile_url", {}).get("value", "")
    co_url  = r.get("company_website_url", {}).get("value", "")
    match   = r.get("amplemarket_person_match", {}).get("value", {})  # rich company/person dict

Key Notes

  • Fuzzy title match: titles are OR-matched loosely. A request for ["CEO", "Founder"] returned a "Product Owner" at one of the test shops. Post-filter on title if strict role matching matters.
  • Location/country filter is highly reliable — use it.
  • Sandbox is free ("environment": "sandbox") — always probe filter shape with 1 record before spending credits. Sandbox returns fake data but validates the request schema.

Cost

3.00 credits per page (up to 100 results). Paying per page — so limit: 5 still costs 3 cr.

DACH SME Hit Rate

  • Company-mode on FE-missed DACH e-commerce SMEs: 28/81 shops (35%) as primary search
  • Avg 2.5 contacts per hit shop, 74 contacts total
  • Company filter accepts both current_employer_names and current_employer_website_urls; domain form works with or without https:// prefix

Provider F: Pipe0 Crustdata (Filter-Based)

Endpoint: POST https://api.pipe0.com/v1/search/run/sync (singular search) Search ID: people:profiles:crustdata@1 Best for: Last-resort fallback after Amplemarket. Also strong for persona searches needing experience/seniority/skill filters.

Request (Company Mode — yes, Crustdata DOES support company filters)

{
  "config": {"environment": "production", "dedup": {"strategy": "default"}},
  "search": {
    "search_id": "people:profiles:crustdata@1",
    "config": {
      "limit": 5,
      "filters": {
        "current_employers_website_urls": {"include": ["example.com"]},
        "current_employers":              {"include": ["Example GmbH"]},
        "current_job_titles":             {"include": ["CEO", "Geschäftsführer"]},
        "locations":                      {"include": ["Germany"]}
      }
    }
  }
}

Filter Format

  • Everything uses the {"include": [...], "exclude": [...]} object form — including current_job_titles. This is the opposite of Amplemarket's plain-array form for titles.

Available Filters (discovered via sandbox probe)

honors, skills, languages, locations,
degree_names, school_names, certifications, fields_of_study,
current_employers, current_employers_website_urls,
current_employers_linkedin_industries,
current_job_titles, current_school_names, current_seniority_levels,
previous_employers, previous_employers_website_urls,
previous_employers_linkedin_industries,
previous_job_titles, previous_seniority_levels,
profile_languages, profile_headline_keywords, profile_summary_keywords,
years_of_experience, years_at_current_company, recently_changed_jobs,
extracurricular_activities

Response

Same shape as Amplemarket. Match field: crustdata_person_match.value.

Cost

5.00 credits per page (100 records). More expensive than Amplemarket — run it AFTER Amplemarket for company-mode fallback, not before.

DACH SME Hit Rate

  • As Amplemarket-fallback on DACH e-commerce SMEs: 1/52 shops Amplemarket missed = very low additive value. Worth skipping if budget is tight.
  • Stronger in persona mode where its experience/skill/certification filters matter.

When NOT to use

  • As primary company-mode search → Amplemarket is 40% cheaper and has comparable DACH coverage
  • If Amplemarket already returned results for the shop → don't double-pay

Output

CSV saved to csv/intermediate/contacts_found.csv:

company_name, company_domain, company_linkedin_url,
first_name, last_name, full_name,
job_title, linkedin_profile_url, source

All original input columns preserved. Always include a source column (e.g. fullenrich_finder, bettercontact_lead_finder, pipe0_amplemarket, pipe0_crustdata, parallel_findall).


Key Rules

  • Default flow: BC → FE fallback. Run BetterContact first (cheaper fixed cost). If BC returns 0 filtered contacts, run FullEnrich as fallback. Track BC and FE credits separately.
  • Two-tier search: Always run Tier 1 (e-commerce/marketing titles). Only run Tier 2 (leadership) if Tier 1 returns 0 AND company is below traffic threshold (e.g. ≤ 200K monthly visits).
  • Global domains require location filter in BetterContact (.co.za safe; .com for global brands returns worldwide).
  • BC is async — submit then poll every 5s until status == "terminated". Typical wait: 30–60s per request. Budget accordingly for large batches.
  • Senior titles bypass keyword filter — "Head of", Founder, Director, C-level always pass regardless of excluded keywords (e.g. "Head of Marketing & Brand" should not be excluded for "brand").
  • Pipe0: use curl via subprocess — Python requests blocked by Cloudflare.
  • Never re-run before reviewing results — save request IDs, poll for results, don't double-submit.
  • Unicode in company names — CSV may use curly apostrophes (\u2019). Use repr() to debug string comparison failures.

Google Sheets Row Matching (when writing results back)

When updating shop/lead status in a Google Sheet, build a lookup map at load time rather than calling ws.find() (which fails on large sheets):

domain_row_map = {}
merchant_row_map = {}
for row_num, row in enumerate(all_values[1:], start=2):
    d = clean_domain(row[col["domain"]])  # strip www./https:// before storing
    if d:
        domain_row_map[d.lower()] = row_num
    m = row[col["merchant_name"]].strip()
    if m:
        merchant_row_map[m.lower()] = row_num

In update_status(), look up by domain first, fall back to merchantName if not found — handles domains stored with www. prefix or other mismatches.


What's Missing (To Document)

  • BetterContact Search (persona mode) — API endpoint and usage
  • Exa Websets via Pipe0 for people discovery
  • Pipe0 Crustdata company-mode hit rates as PRIMARY search (currently only measured as AM-fallback)

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.