People Enrichment
People enrichment is the step that turns a resolved LinkedIn URL into a complete, trustworthy contact profile. It reads the global people intelligence cache first, runs a fixed-order provider waterfall only on what is missing, and writes every resolved fact back with provenance - so the first org to enrich a person pays once and every later search rides free. Headhunter and People Signals discover who; this fills them in.
What enrichment does
tl;drDiscovery finds a person worth contacting and resolves them to a canonical LinkedIn URL - Headhunter from a brief, People Signals from a signal. Enrichment is the separate step that fills the profile, contact and employment facts they need - cheaply, and only once across all tenants. It is a cache-first read in front of the people intelligence database: a fresh hit costs nothing, a miss runs the waterfall and writes back with provenance.
Headhunter is the search - criteria in, a ranked candidate set out. enrich_person() is the fill - one identity in, one complete profile out, cache-first and written back. Splitting the two means there is exactly one enrichment path, shared by Headhunter, People Signals, signal monitoring and the CRM save - so the expensive part of learning a person is paid once and reused everywhere at $0.
Data flow
interactiveHow a single enrich_person() call moves through the system. Pick a path - a cache hit, a miss that runs the waterfall and writes back, or a job-change re-verify - and the diagram lights the nodes and edges it touches, in order. Click any node for detail.
Fresh hit: the facade finds an in-TTL profile in intel_people and serves it straight into the result and the tenant pre-fill. Zero provider calls, $0 spend.
Click any node for detail. The write-back path is what turns a per-org cost into a one-time cost: the first tenant to touch a person runs the waterfall, the result lands in intel_* with provenance, and every later run by any tenant follows the cache-hit path at $0. The pre-fill into the tenant CRM is one-way by construction - nothing tenant-derived ever travels back up.
Cache-first read
interactiveThe core mechanic: enrichment asks the intelligence DB first, the waterfall runs only on a miss, and every miss makes the cache smarter. Pick a scenario and watch the lookup path - including a job change, the one event that deliberately invalidates a fresh row.
Person facts age at different speeds, so freshness is checked per field group: the hot employment edge and title (~30 days), warm work-email re-verification and skills (~90 days), cold education (~365 days). A lookup never blocks on a refresh - stale data is served immediately and a background job re-runs just the expired tier. A job-change signal can force-expire the hot tier early.
Enrichment waterfall
interactiveOn a miss the waterfall runs in fixed order - cache, then cheap search, then structured extraction + email, then web fallback. The model never reorders tiers; its only decision is escalate-or-stop against a completeness target, so the run stops the moment the profile is good enough. Pick a tier to inspect it.
Intelligence DB cache Hit: skip all paid tiers
A global people-facts cache keyed by the normalized LinkedIn URL, TTL per field group. A hit returns the profile, current employer edge and work email instantly and skips every paid tier - the cross-org payoff where the first org to enrich a person pays, and every later search rides free.
Every resolved field carries the reputation of the tier that produced it. When two tiers disagree, the higher reputation wins - a structured provider value is never silently overwritten by a web-inferred one, and an unverified web value is stored low-reputation so it caps its own contribution to the downstream lead score.
Provider categories
interactiveThe structured tier is a registry of enrichers grouped by what they fill. Each is an enricher, never a discovery source - Headhunter's search runs first, then enrichment calls these on the one resolved person. Pick a category to see what runs in production today and the vetted candidates that would close its gaps.
The durable person record - name, current title, seniority, location, skills, keyed on the LinkedIn URL.
Running today
- Exa - Neural profile / company search; the cheap first paid tier.
- Firecrawl - Structured extraction from the public profile page. In production today.
Recommended next
- People Data Labs - 1.5B+ person profiles; structured employment and skills via API.
- Proxycurl - LinkedIn-shaped person + company endpoints, pay-per-lookup.
Privacy and erasure
seam Enrichment is the main writer into the shared layer, so the seam matters most here. It is enforced twice: by policy (only publicly observable professional facts plus discovered work emails may be written back) and by mechanism (client roles get RLS deny-all on intel_*; the ac-python-api service role is the only door).
intel_peopleintel_person_employmentsintel_sourcesEvery fact the waterfall resolves flows up into the shared layer, tagged with source and freshness.
The public-fact profile pre-fills crm_people enrichment fields at save time.
Tenant judgement and workflow state never enter the shared layer, even aggregated.
crm_peopleworkflow_run_peopleshortlists · outreach · notes- Public professional profile: name, current title, seniority, location
- Employment history edges joined to intel_companies
- Verified work emails, tied to the current employer edge
- Public posts, speaking, thought leadership
- CRM stage, shortlist, outreach history, notes, tasks
- Whether any tenant saved, rejected or contacted a person
- Per-tenant ICP scores, lead scores, fit judgement
- Anything derived from tenant activity, even aggregated
Enriching people stores personal data, so deletion is a build requirement, not a follow-up. A hard delete spans the layer plus a SHA-256 suppression hash that blocks re-ingestion; the intel_sources rows a person references are the data-subject-access inventory and the Article 14 source lineage. Caching paid provider output cross-tenant (Hunter in particular) needs a terms review before write-back is enabled for it.
What enrichment writes
modelThree tables carry the output. The profile is the fast read path the next run hits; the employment edge models a career as append-only history so a job change supersedes rather than overwrites; sources keep every claim explainable and revocable in one delete. Click a table.
intel_peopleFlat canonical profile - the fast read path enrichment fillslinkedin_urltext · uniquenormalized, primary identity key - never the employerfull_name · headline · title · senioritytextlocation · avatar_urltextskillstext[]extrajsonblong-tail provider fields until usage earns a columnfetched_hot_attimestamptzemployment + title TTL (~30d)fetched_warm_attimestamptzwork email re-verify, skills (~90d)fetched_cold_attimestamptzeducation (~365d)Structured provider payloads (Hunter, People Data Labs)
highest confidence, contractual accuracy
First-party extraction of the public profile (Firecrawl)
public and current, less structured
Web-inferred (GPT web_search)
lowest confidence, never overwrites a higher rank silently