Source people in bulk from Anysite's LinkedIn profile database (search_sql_users) with ~60 filters - derived seniority/function, company domain/id incl. past employers (alumni), career-shape (new in role, tenure, promotions), education, skills, deterministic territory buckets. Use when the user wants to find people in bulk - persona lists, buyers/candidates by role and company traits, competitor alumni, "new VP hires", recruiting searches - "найди людей", "собери список персон", "кто недавно ...
Installs into .claude/skills of the current project.
Are you the author of Anysite People Sourcing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anysiteio-anysite-people-sourcing)
---
name: anysite-people-sourcing
description: Source people in bulk from Anysite's LinkedIn profile database (search_sql_users) with ~60 filters - derived seniority/function, company domain/id incl. past employers (alumni), career-shape (new in role, tenure, promotions), education, skills, deterministic territory buckets. Use when the user wants to find people in bulk - persona lists, buyers/candidates by role and company traits, competitor alumni, "new VP hires", recruiting searches - "найди людей", "собери список персон", "кто недавно сменил работу". For company lists first use anysite-company-sourcing; for one known person use linkedin/user; for CRM push use anysite-crm-prospect.
---
# People Sourcing
`linkedin/search/sql/users` (endpoint `search_sql_users`) searches a very large
profile database with filters no live LinkedIn search has: derived seniority and
function, career-shape maths (tenure, promotions, months in role), past-employer
alumni, company domains and deterministic bucketing. It is the bulk people
workhorse; live `search_users` stays the tool for one-off lookups and namesake
disambiguation.
Verified live: filters compose correctly (seniority_min=head + function=sales +
US + 51–500 headcount + new-in-role returned exactly that, role histories fresh
to the current quarter; alumni via `past_company_id` returns people whose role at
that company has an end date, never current staff).
## Two semantics that change how you work
1. **An over-`count` result is an unbiased SAMPLE, and repeating the request
returns the SAME people** (verified: two identical calls → identical 5
aliases; and raising `count` only appends — the list is prefix-stable, not
reshuffled). Calling again is not pagination.
2. **Buckets are disjoint and stable** (verified: `bucket_total:2`, index 0 vs 1
→ zero overlap across 40). Walk them for volume: `bucket_total:N,
bucket_index:0..N-1`, also the built-in territory splitter. **Ceiling:**
`bucket_total` max is 100, so the reachable population is ~100×1000 = 100k;
size `bucket_total` so each bucket is < 1000, otherwise a bucket is itself a
sample and you silently lose the tail.
## What filters but does NOT come back (read before you "validate")
The derived fields you filter on are **not in the response**: no `seniority`,
`function`, `profile_score`, `months_in_role`, `promotion_count`, `industry`,
`employee_count` (verified on a live record). Consequences:
- "Validate against intent" = read `experience[]` by hand: take roles with
`end_date == null` (the current ones) and check `.position` / `.company.name`.
You cannot re-sort or re-segment the result by seniority/function via
`query_cache` — those columns aren't there.
- The only size field carried per role is `experience[].company.employee_range`,
and that string is unreliable (see Company filters). There is no trustworthy
company-size value in a people result to write to a CRM.
## Filter map (grouped; full contract via `discover(linkedin, search)`)
- **Batch identity:** `alias[]`, `urn[]`, `member_id[]`; `last_name[]`
(diacritics/case/spacing-folded) + `first_initial[]` for "J. Smith".
- **Text DSL** (whitespace=AND, `|`=OR no spaces around it, `"phrase"`, `-not`):
`name`, `headline`, `summary`, `current_title`, `any_title` (past roles
included), `skills`, `languages`, `location`, `keywords`, `edu_*`.
**IRON RULE — quote every multi-word alternative in an OR chain.** Whitespace
binds tighter than `|`, so an unquoted phrase SHATTERS the chain into
impossible AND groups: `current_company_name: "Tencent|Tencent Games|Level
Infinite|Proxima Beta"` parses as `(Tencent|Tencent) AND (Games|Level) AND
(Infinite|Proxima) AND Beta` → **0 results on Tencent-scale companies**
(verified live — this exact failure burned a real user session). Correct:
`"Tencent"|"Tencent Games"|"Level Infinite"|"Proxima Beta"|Krafton`.
Lint before EVERY send: any token containing a space inside an OR chain
without quotes → do not send, fix first.
- **Derived seniority/function — start here for recall.** `seniority` /
`seniority_min` (entry→ic→senior_ic→manager→head→vp→founder→cxo), `function` /
`any_function` (sales, marketing, engineering, product, data, finance, hr, ops,
legal, support, exec, …). Catch titles a text search can't (CRO, CCO, "VP
Business Development" all matched a `function:sales`+`seniority_min:vp` query —
none would survive a `"vp of sales"` DSL). Two traps, both measured:
- **`seniority_min:"head"` includes `founder`** → pulls solo owners of 2-person
shops. For enterprise buyers list the levels explicitly:
`seniority:["head","vp","cxo"]`, and always add `employee_count_min`.
- Token false-positives: "CEO Office" (staff, not the CEO) scored ≥head;
"…President's Club 2020…" (an award on a rank-and-file AE) scored ≥vp.
Precision on a vp+sales slice was 9/10 — good, not perfect; verify the
shortlist against `experience[]`.
- **Company — a ladder, pick the rung deliberately:**
1. `current_company_name` DSL with QUOTED brand variants — the default. A bare
token like `Tencent` also matches subsidiaries ("Tencent Korea", "Fintech at
Tencent") — a feature for brand families, a bug if you need one legal entity.
2. `current_company_id[]` (numeric LinkedIn page ids, resolve via company
search first) — when strictness matters: separating Level Infinite from the
rest of Tencent, excluding brand-name lookalikes.
3. `current_company_domain[]` (bare domains) — when domains are what you have
(e.g. a CRM list).
4. `keywords` — LAST resort, only WITH ≥1 selective filter (company/title/
seniority/country) and never as the employer filter itself: it's full-text
across all fields, and an unselective keywords query is a DB scan
(measured: 140s → HTTP 500).
Also: `any_company_id/domain` (ever worked);
**`past_company_id[]` (worked and LEFT — alumni)**; `employee_count_min/max` of
the current company. **Do NOT use `employee_range[]`** — the band string
contradicts the true headcount often (measured ~35%: a 104-person company
banded "11-50", a 58-person one "501-1000"); filter size only with the numeric
`employee_count_min/max`.
- **Industry:** `industry[]` / `any_industry[]` take **exact LinkedIn-taxonomy
labels** (e.g. "Software Development" works), NOT market words — `["fintech"]`
returns 0 (measured). Resolve labels via `search_industries`; for "fintech",
"SaaS" and the like, filter on the company side (`anysite-company-sourcing`
specialities) or on `headline`/`skills` DSL instead.
- **Geo:** `country[]` ISO2 (reliable); `location` DSL for cities/metros.
**"Responsible for a market" ≠ "located in it."** When the user wants people
FOCUSED on a region (CIS/MENA/emerging markets), do NOT filter `country` by
that region — BD/publishing teams of global companies sit in HQ (KR/CN/US/NL/
SG). Search the market mention in the profile instead:
`headline`/`summary`: `CIS|Russia|Kazakhstan|MENA|"emerging markets"|"Middle
East"`. Measured on a live case: `country:[RU,KZ,UZ,AE,SA,…]` → 2 profiles;
the headline variant → 84. Use `country` only when the person must physically
BE in the region — and warn the user the pool will be narrow.
- **Education:** `edu_slug[]`, `edu_institution`, `edu_field`,
`edu_ended_year_min/max` (graduation cohort ≈ age proxy).
- **Career shape:** `months_in_role_max` (new in role), `months_in_role_min`,
`months_since_change_max` (started OR ended a role recently — job-change
radar), `experience_years_min`, `n_roles_*`, `n_companies_*` (stability vs
hopping), `promotion_count_min` (promoted without changing employer),
`avg_tenure_months_*`.
- **Quality & coverage:** `profile_score_min` (0–8) — do NOT set it by default:
it silently drops sparse-but-on-target profiles (a real user's default of 5
cut exactly the people they were hunting). Add it only when the user asks for
"complete/outreach-grade profiles" (then 5+), or as a tie-breaker on an
over-1000 pool. Plus the `has_*` family (`has_current_role`, `has_role_dates`,
`has_education`, `has_company_size`, `has_engagement`…).
- **Badges/engagement:** `open_to_work`, `hiring`, `verified`, `is_premium`,
`is_top_voice`, `follower_count_min`, `connection_count_min`. (`is_creator` /
`is_influencer` filter but are NOT returned — you can't confirm them.)
- **Lookalike (EXPERIMENTAL — verify every time):** `similar_to` / `also_viewed`
by alias. Measured unreliable: two seeds returned namesakes (same name,
different people) or nothing — NOT semantic lookalikes. Do not use it as a
"people like our champion" shortcut; if you try it, probe `count:5` and confirm
the surnames differ from the seed's before trusting a single row. For real
lookalikes, replicate the champion's attributes as explicit filters instead.
- **Freshness:** `fetched_after` (unix ts) — only recently refreshed profiles.
## Coverage honesty
Not every profile states a current employer, industry, company size or dated
roles — those filters answer **only for people who do**. The API's own numbers:
current title, seniority and function resolve for about a third of people;
`current_company_id`, `current_company_domain` and `employee_range` for about one
person in five. So a company-id or domain rung returns roughly a fifth of the
people who really work there — say so, and for coverage run the
`current_company_name` rung alongside it. Set the matching `has_*` flag
deliberately and tell the user the trade-off ("filtering by company size skips
people whose company doesn't state one"). `open_to_work: false` = "no badge
observed", not "not looking". Follower/connection counts exist on a small share —
gate with `has_engagement`.
Two data-shape facts to expect: some profiles carry **multiple concurrent open
roles** (measured: 4/10 in one slice; one person had ~19 — advisory boards,
visiting posts). "Current company" is then ambiguous, and a
`current_company_domain`/`employee_count` filter may match on a quarter-time
advisory role, not the day job — so verify the target company is an *operational*
current role before anchoring an opener on it. And `urn` is occasionally `null`
even when `alias` is present — the vanity URL still enables `user_find_email_by_url`
and `user_posts` (it accepts the alias/URL and resolves it at the cost of one extra
lookup).
## The loop
1. **Start from the most selective structural filters** (seniority/function/
company/country) — never bare `keywords` (it only helps for distinctive words
and must ride a selective filter). No persona in the request → use the titles,
seniority and function from the Personas section of `anysite-gtm-profile` when
it exists.
2. **Probe `count:10`, validate against INTENT via `experience[]`** (structural
filters aren't semantic: "Director of Sales, 51–200, US" returned a coffee
wholesaler). Precision comes from `current_company_domain[]` (a list from
`anysite-company-sourcing`), taxonomy `industry[]`, or headline/skills DSL.
3. **Tighten/widen, re-probe, fetch;** >1000 → size `bucket_total` so each bucket
< 1000 and walk it.
4. **Free re-cuts** with `query_cache` (only on returned fields); export with
`export_data`. In clients that render MCP Apps, show the result with
`show_entity_table(cache_key, group_by="company_id")` so people are grouped by
account; people at approved accounts from `review_leads` start from that
selection's company ids.
**Zero results — debug in this order, don't flail:**
1. Re-read your own DSL for unquoted multi-word phrases in OR chains (the #1
cause by far — see the IRON RULE).
2. Loosen ONE filter per step (drop `profile_score_min`, widen `seniority`,
drop `industry`) — never jump straight to a bare `keywords` full-text.
3. Sanity check: 0 results for a Tencent-scale employer is almost certainly a
query bug, not missing data — go back to step 1, don't report "not found".
**500/408 on a broad query:** don't retry as-is — narrow it (add a selective
filter) or raise `timeout` (≤1500). An unselective query is a scan; retrying a
scan just times out again.
## Recipes
- *"VPs of Sales at US SaaS 50–500"*: `seniority:["vp","cxo"], function:["sales"],
country:["US"], employee_count_min:50, employee_count_max:500,
industry:["Software Development"]`. (SaaS ≠ a taxonomy label — narrow via the
company-side list or headline DSL. `profile_score_min` only if asked.)
- *"BD/partnerships across a brand family"* (verified live, 99 profiles):
`current_company_name: "\"Tencent\"|\"Tencent Games\"|\"Level Infinite\"|
\"Proxima Beta\"|Krafton", current_title: "\"business development\"|bizdev|
partnership|partnerships|licensing|monetization|payments|commercial",
seniority:["manager","head","vp","cxo"], has_current_role:true` — every
multi-word alternative quoted; regional focus goes in `headline`, not
`country` (see Geo).
- *"New decision-makers"* (best-converting timing): add `months_in_role_max:3`
(default 90 days). Too few → widen the title family first, then relax size or
geography, and only then the window (6 months; 9 is the ceiling).
- *"Buying committee at one account"*: `anysite-buying-committee`.
- *"Warm paths"*: people who used to work at your customers —
`past_company_id:[<customer ids>]` + your persona filters; name the shared
employer in the message only if the user confirms it may be mentioned.
- *"Grew up inside the company"* (internal champion / stable ABM contact):
`promotion_count_min:2` + `avg_tenure_months_min:24`.
- *"Competitor alumni"*: `past_company_id:[<ids>]` (+ function/seniority) — people
who LEFT. Note: this is ex-employees, NOT the competitor's customers; for
customers use `anysite-crm-competitor-intel` (wappalyzer + reviews).
- *EMEA localized sequences*: `languages:"german", country:["DE","AT","CH"]`.
- *School alumni warm-intro*: `edu_slug:[...]` + `edu_ended_year_min/max`.
- *Recruiting*: `skills`/`edu_field` DSL + `avg_tenure_months_min` (stability) +
`n_companies_max` (not a hopper) + `open_to_work:true` as a bonus tier only.
- *Territory split*: same query, `bucket_total:<reps>, bucket_index:<rep #>`.
## Handoffs and hygiene
- Company lists IN (`current_company_id`/`domain`) come from
**`anysite-company-sourcing`**; people OUT go to **`anysite-crm-prospect`**
(dedup + CRM push).
- For "up to N contacts across a few companies", 2–3 focused queries (base +
regional + function-specific) deduped by `alias` beat one mega-query — each
stays selective and each angle surfaces people the others rank low.
- **Emails have a real cost and yield — don't hand-wave it.** The result's
`alias` is the vanity URL, ready for the email cascade (`anysite-mcp` →
Email finding): `user_email` first (cheap, ~half personal addresses, low
yield), then `user_find_email_by_url` (~75% yield but 50cr each — estimate
`50cr × N` and confirm before a big run) with its `valid_email`/`email_status`
as the deliverability gate. Personal/unverified addresses never go into a work
sequence. For bounce-critical sends, run the address through `emails/verify`
(`status` valid / invalid / risky, `is_personal`); it may answer from a verdict
stored up to a year ago (`resolved_by` says which).
- **Live-verify the outreach shortlist** via `linkedin/user` with a small
`cache_max_age_days` (e.g. 7) — without it the call may return a profile up to
180 days old, which defeats the check; it also yields the vanity URL. Job-change detection on CRM
contacts belongs to `anysite-crm-champions` (this DB's `months_since_change_max`
is its cheap pre-filter, not its evidence).
- Sourcing individuals is personal-data processing: search to the stated business
need, don't hoard.