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
- An over-
countresult is an unbiased SAMPLE, and repeating the request returns the SAME people (verified: two identical calls → identical 5 aliases; and raisingcountonly appends — the list is prefix-stable, not reshuffled). Calling again is not pagination. - 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_totalmax is 100, so the reachable population is ~100×1000 = 100k; sizebucket_totalso 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 withend_date == null(the current ones) and check.position/.company.name. You cannot re-sort or re-segment the result by seniority/function viaquery_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 afunction:sales+seniority_min:vpquery — none would survive a"vp of sales"DSL). Two traps, both measured:seniority_min:"head"includesfounder→ pulls solo owners of 2-person shops. For enterprise buyers list the levels explicitly:seniority:["head","vp","cxo"], and always addemployee_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:
current_company_nameDSL with QUOTED brand variants — the default. A bare token likeTencentalso matches subsidiaries ("Tencent Korea", "Fintech at Tencent") — a feature for brand families, a bug if you need one legal entity.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.current_company_domain[](bare domains) — when domains are what you have (e.g. a CRM list).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/maxof the current company. Do NOT useemployee_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 numericemployee_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 viasearch_industries; for "fintech", "SaaS" and the like, filter on the company side (anysite-company-sourcingspecialities) or onheadline/skillsDSL instead. - Geo:
country[]ISO2 (reliable);locationDSL 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 filtercountryby 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. Usecountryonly 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 thehas_*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_influencerfilter but are NOT returned — you can't confirm them.) - Lookalike (EXPERIMENTAL — verify every time):
similar_to/also_viewedby 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, probecount:5and 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. 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,
but URN-keyed handoffs (user/posts) need the live linkedin/user step first.
The loop
- 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). - Probe
count:10, validate against INTENT viaexperience[](structural filters aren't semantic: "Director of Sales, 51–200, US" returned a coffee wholesaler). Precision comes fromcurrent_company_domain[](a list fromanysite-company-sourcing), taxonomyindustry[], or headline/skills DSL. - Tighten/widen, re-probe, fetch; >1000 → size
bucket_totalso each bucket < 1000 and walk it. - Free re-cuts with
query_cache(only on returned fields); export withexport_data.
Zero results — debug in this order, don't flail:
- Re-read your own DSL for unquoted multi-word phrases in OR chains (the #1 cause by far — see the IRON RULE).
- Loosen ONE filter per step (drop
profile_score_min, widenseniority, dropindustry) — never jump straight to a barekeywordsfull-text. - 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_minonly 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 inheadline, notcountry(see Geo). - "New decision-makers" (best-converting timing): add
months_in_role_max:6. - "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 useanysite-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_fieldDSL +avg_tenure_months_min(stability) +n_companies_max(not a hopper) +open_to_work:trueas 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 fromanysite-company-sourcing; people OUT go toanysite-crm-prospect(dedup + CRM push). - For "up to N contacts across a few companies", 2–3 focused queries (base +
regional + function-specific) deduped by
aliasbeat 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
aliasis the vanity URL, ready for the email cascade (anysite-mcp→ Email finding):user_emailfirst (cheap,half personal addresses, low yield), then75% yield but 50cr each — estimateuser_find_email_by_url(50cr × Nand confirm before a big run) with itsvalid_email/email_statusas the deliverability gate. Personal/unverified addresses never go into a work sequence. There is no built-in email verifier — for bounce-critical sends, gate externally. - Live-verify the outreach shortlist via
linkedin/user— the DB is fresh but not realtime; it also yields the vanity URL. Job-change detection on CRM contacts belongs toanysite-crm-champions(this DB'smonths_since_change_maxis its cheap pre-filter, not its evidence). - Sourcing individuals is personal-data processing: search to the stated business need, don't hoard.