Enrich existing CRM records (HubSpot contacts and companies) with fresh data from anysite - job titles, LinkedIn profiles, firmographics, emails. Reads records from the connected CRM, finds gaps in mapped fields, fills them from LinkedIn/Crunchbase/web sources, and writes back safely (fill-blank, dry-run, undo). Use when the user asks to enrich CRM records, fill missing fields, update contact or company data, or refresh a CRM list. Requires an active CRM connection and the anysite-crm-profile...
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anysiteio/agent-skills --skill anysite-crm-enrich --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Anysite Crm Enrich?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anysiteio-anysite-crm-enrich)More formats (shields.io, HTML) on the badges page.
---
name: anysite-crm-enrich
description: Enrich existing CRM records (HubSpot contacts and companies) with fresh data from anysite - job titles, LinkedIn profiles, firmographics, emails. Reads records from the connected CRM, finds gaps in mapped fields, fills them from LinkedIn/Crunchbase/web sources, and writes back safely (fill-blank, dry-run, undo). Use when the user asks to enrich CRM records, fill missing fields, update contact or company data, or refresh a CRM list. Requires an active CRM connection and the anysite-crm-profile mapping.
---
# CRM Enrich
Fill gaps in existing CRM records with anysite data. The most common flow: pull a list from
the CRM, enrich only what is missing, write back only to mapped fields.
## Prerequisites
1. `crm_list_connections` → an `active` connection. Missing → send the user to
`/anysite-crm-setup` (or Profile → CRM Integration in the dashboard).
2. The `anysite-crm-profile` skill exists → its mapping is law. Missing → minimal safe mode:
standard properties only, recommend running setup.
3. Read the Writing rules in `anysite-crm-setup` — they apply to every write below.
## Flow
### 1. Scope — what to enrich
Ask (or infer from the request): which records and which fields. Pull them:
```
crm_query_records(object_type="contacts", list_id=<working list> | search=... | emails=[...],
properties=[record_id + match keys + mapped target fields])
```
Page through everything in scope. Locally split records into:
- **complete** — all mapped target fields filled → skip (report count),
- **enrichable** — has a match key (email / linkedin_url / domain) and gaps,
- **unmatchable** — no key at all → report, do not guess identities.
### 2. Resolve identities (contacts)
- Has `linkedin_url` → `execute linkedin/user/user` (full profile: title, company, location).
- Only email → try reverse lookup first: `execute linkedin/email/email_sql_user` (cached,
cheap) → remainder via `email_user` (live). Both take ONE email per call — loop, don't
batch, and expect misses (verified to return empty even for people who are on LinkedIn).
Then the cascade that actually works, because the CRM knows the name: email domain →
resolve company (verified, per anysite-mcp recipe) → `organizational_urn` →
`search_users {first_name, last_name, current_company: [{"type": "company",
"value": "<id>"}]}` → usually exactly one match, WITH the profile URN as a bonus.
Company filter mandatory — bare names return namesakes. On BIG batches, do the same
cheaper in one call: `search_sql_users {last_name: [...], current_company_domain:
[<email domains>], count}` resolves many name+domain pairs at DB cost (then live-verify
what you'll write). Still nothing → leave record, report.
- Needs email → cascade: `user_email` (batch ≤10, cheap, low yield, a MIX of personal and
work addresses incl. past employers — group by profile, match domain to current company)
→ remainder via `user_find_email_by_url {url: <vanity profile URL>}` (50cr
each — estimate cost on large lists first). Check `valid_email`/`email_status` in its
response and write only addresses that pass; report the rest as "found, unverified".
Re-use cache instead of re-fetching anything twice — including across sessions: `search_requests` (free) finds cache_keys of identical calls from the last 7 days (a company resolved yesterday doesn't need a paid re-resolve today).
### 3. Resolve companies
- By domain — with MANDATORY exact verification on every resolve (the `website` search is
substring match: stripe.com → Soundstripe; stlabs.com → five other *labs.com companies).
Default one domain per call; OR-DSL batching is an optimization with a tax — any domain
that didn't come back exact-matched gets re-queried individually:
```
execute linkedin/search/search_sql_companies {website: "acme.com", count: 5}
# batched variant: {website: "acme.com|globex.io", count: 10× domains}, then per domain:
query_cache {conditions: [{"field": "website", "op": "=", "value": "acme.com"}],
limit: <fetched count>}
# query_cache filters over the WHOLE cached set; `limit` (default 10) caps only how many
# rows come BACK. One domain → the default is fine; a multi-domain batch → pass a limit.
```
Only an exact-website match (normalized: lowercase, no protocol/www/path) counts as
resolved. Gives industry, employee_count, description, locations.
**No exact match? Do NOT stop there** — a whole class of domains never appears in its own
substring results (verified: `{website: "stlabs.com", count: 5}` returns five other
*labs.com companies and never STLabs, a live company with a LinkedIn page). Standard second
step, ~1cr:
```
execute webparser/parse {url: "https://acme.com", extract_minimal: true}
→ top-level `title` = who they say they are
→ links[] usually carries their own linkedin.com/company/... URL
execute linkedin/company {company: "<that URL>"} # exact, no fuzzy matching
```
Only after that fails is the domain genuinely unresolved — report it, never write.
- Deeper firmographics (funding, size range): take the alias from `crunchbase_link`, which
the domain-resolve above ALREADY returned — free. Only when that field is empty and the
company is plausibly venture-backed, fall back to the live `crunchbase/search` by name
(20cr, fuzzy — verify name+domain before trusting it):
```
# crunchbase_link: "https://www.crunchbase.com/organization/acme" → alias "acme"
execute crunchbase/company {company: "acme"}
```
Only when the profile maps such fields. Normalize `contacts.email` before use — trailing
dots observed ("founders@reducto.ai."), and a match key with a trailing dot matches nothing.
### 4. Write back
Build upsert records keyed by the CRM `record_id` you pulled in step 1 — never re-search
the CRM for a record you already hold, and never create from an enrich flow:
```
crm_upsert_contacts(records=[{record_id, properties:{<mapped fields only>}}],
allow_create=false,
overwrite_properties=[<only fields marked overwrite in profile>],
dry_run=true)
```
Show the diff (old → new, counts of fill/skip), get confirmation, re-run with
`dry_run=false`. Companies go through `crm_upsert_companies`, which matches ONLY by
`domain` — always include `domain` in the properties you pull in step 1; a company record
without a domain is unwritable (report it, don't improvise a match). Keep request batches
reasonable (≤100 records per call). Save the returned `run_id`.
### 5. Report
Written / filled-blank-skipped (`fill_blank_skip` = policy working, not an error) /
enum warnings / unmatchable. Mention `crm_undo(run_id)` availability. If the profile maps
`anysite_last_enriched_at`, it was stamped by the mapping — say so.
## Rules specific to enrichment
- Never write a value you did not get from a source this session. No invented data.
- A profile field with no fresh source value → leave it out of `properties` entirely.
- Enum targets (e.g. industry): pick from the CRM schema `options` list, translating the
source value; no match → skip with a note, don't force.
- >10 records or any overwrite → dry-run first, always.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!