Systems Lab

Agent skill

crm-enricher

Fills missing emails, mobiles, job titles and company fields in a HubSpot, Pipedrive, GoHighLevel or Salesforce export using ScraperCity Lead Database lookups (by companyDomain, companyName or socialUrl), Email Finder, Mobile Finder and Website Finder, with one cost-ordered ladder per field and Tech Stack Search flags for technographics.

activeNeeds a keyActs undeclared5,445 words

Filed under CRM and RevOps and Prospecting and list building.

From scrapercity/gtm-skills · 31 skill entries · 0 · pushed 2026-11-02

What it does when it runs

Fills missing emails, mobiles, job titles and company fields in a HubSpot, Pipedrive, GoHighLevel or Salesforce export using ScraperCity Lead Database lookups (by companyDomain, companyName or socialUrl), Email Finder, Mobile Finder and Website Finder, with one cost-ordered ladder per field and Tech Stack Search flags for technographics. Applies a field-level fill precedence that never overwrites owner-entered data without a stated rule, scores every match, flags each unfillable field with a reason code, adds write-back audit columns, maps the file back to the CRM's import format and sets a refresh schedule. Use when the user says "enrich my CRM", "this export is half empty", "fill in missing emails and phones", "append titles and company size", or "update our HubSpot records".

Automated analysis of the skill and the 13 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.

Keys and connectors you must supply
  • SCRAPERCITY_API_KEY
Hosts it reaches
  • alexberman.com
  • app.scrapercity.com
  • aws.amazon.com
  • blog.google
  • datatracker.ietf.org
  • developer.salesforce.com
  • emt.gartnerweb.com
  • help.gohighlevel.com
  • help.instantly.ai
  • help.salesforce.com
  • helpcenter.smartlead.ai
  • knowledge.hubspot.com
  • leanb2bbook.com
  • publicsuffix.org
  • resources.docs.salesforce.com
  • scrapercity.com
  • senders.yahooinc.com
  • support.google.com
  • support.pipedrive.com
  • techcommunity.microsoft.com
  • www.bls.gov
  • www.bmj.com
  • www.crexi.com
  • www.gov.uk
  • www.itl.nist.gov
  • www.linkedin.com
  • www.rfc-editor.org
  • www.twilio.com
  • www.zerobounce.net
  • zerobounce.net
Tool permissions it declares
No allowed-tools in the frontmatter. It does act, so it runs under whatever permissions your session already grants.
Actions present in the files
writes files

Ask about crm-enricher

Opens your assistant with this page's recorded context. Check the linked sources before relying on it.

Is this safe to install?ClaudeChatGPT
Adapt it to my stackClaudeChatGPT
What else do I need for it to workClaudeChatGPT
Rather ask a human? Talk to Cheetah
git clone --depth 1 --filter=blob:none --sparse https://github.com/scrapercity/gtm-skills.git /tmp/gtm-skills
git -C /tmp/gtm-skills sparse-checkout set "skills/list-building/crm-enricher"
mkdir -p ~/.claude/skills/crm-enricher
cp -R "/tmp/gtm-skills/skills/list-building/crm-enricher/." ~/.claude/skills/crm-enricher/

Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗

Or take the whole library

This repo ships a .claude-plugin manifest, so Claude Code can install all 31 skills at once. Plugin skills are invoked as /<plugin>:<skill>, so they never collide with your own.

/plugin marketplace add scrapercity/gtm-skills
/plugin

The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.

Before you install: this skill will not complete its job on a bare agent. It needs SCRAPERCITY_API_KEY, which you have to obtain separately.

Reproduced in full from scrapercity/gtm-skills/blob/46d446143690c8048b17817aecf36990ea6880e5/skills/list-building/crm-enricher/SKILL.md, which is licensed MIT (skill frontmatter). 5,445 words, 27 headings.

CRM Enricher

Overview

This skill takes a CRM export with holes in it and returns the same rows with gaps filled under written rules, an update file and a create file mapped to the user's CRM, and a report that accounts for every field it touched or could not fill and dates the next refresh.

The mistake it prevents is the "enrich everything, overwrite everything" import. A rep typed a direct dial and a nickname into a record last month; an enrichment job replaces both with a switchboard and a legal name, the import runs, and the team cannot tell which values changed. This skill treats every CRM value as owner-entered until proven stale, writes found values to empty fields only unless a field rule says otherwise, keeps the prior value in an audit column whenever it does replace one, and refuses to auto-write a low-confidence match.

It also prevents paying twice. The Lead Database lookup runs first because it is included on Growth and Scale plans; Email Finder ($0.05 per email found) and Mobile Finder ($0.25 per number found) run only on the rows the database could not fill.

When to Use

  • "Here's our HubSpot export, 40% of contacts have no email. Fill what you can."
  • "Append job titles and company size to these Pipedrive people."
  • "Add mobiles for our open opportunities before the call blitz."
  • "Our Salesforce leads are stale. Which ones changed jobs?"
  • "Clean up this GoHighLevel import before we load it."

Use a different skill when:

The user wantsUse
Only to validate the emails already in a listemail-list-cleaner
Only mobiles for a call list (no CRM write-back)mobile-appender
New contacts at accounts that have none, by buying roledecision-maker-finder
A brand-new list from an ICPicp-list-builder
A send-ready cold email file from these recordscold-email-list
Lead Database parameters in depthscrapercity-lead-database

Prerequisites

InputWhyDefault if unknown
CRM name and object (contacts, leads, people, companies)Picks the import identifier and column mapAsk
The export CSV with the CRM record ID columnUpdates must match on ID, not on nameAsk for a re-export that includes the ID
Fields to fill, in priority orderSets scope and costemail, job title, seniority, department, company size, industry; mobiles only if asked
Scope for mobiles: accounts or stages, plus the seniority levels and departments reps will dialMobiles cost $0.25 each when found; every number bought for someone reps will not call is wastedAsk. The seniority and department filter runs on the Lead Database's refreshed seniority and department values, so mobiles run after the title refresh
Value of one email and one mobile to the userDecides whether a lower-yield pass (Email Finder retry, Website Finder) is worth runningAsk; a pass stays only when its cost per usable result is at or under this value
Who owns which fieldsDecides overwrite policyEvery non-empty CRM value is owner-entered
Plan levelLead Database needs Growth ($149/mo) or ScaleRun check_wallet and read plan.name
Suppression flagsRows that skip every paid lookupAny opt-out, DND or unsubscribed field in the export marks the row suppressed
Freshness windows for titles and company sizeA CRM value older than the window can be staged for replacementAsk; until set, every differing value is staged

Authoritative Foundations

SourceWhat it contributes here
DAMA UK data quality dimensions (UK Government Data Quality Hub, 2021)Accuracy, completeness, uniqueness, consistency, timeliness and validity. The report measures completeness before and after, validity (E.164, email status), and timeliness (enriched_at, validation date).
HubSpot import docsRecord ID overrides other identifiers; contacts match on email, companies on Company Domain Name; three modes (create and update, create only, update only); a per-property "Don't overwrite" checkbox.
Pipedrive import docsPeople merge when name plus organization, phone or email matches; organizations merge on address; merge step can be bypassed.
HighLevel import docsUpdate or Create and Update; matching order Contact ID, then email, then phone; a "Don't update Empty Values" checkbox.
Salesforce Data Import Wizard and Data Loader docsAdd, update, or add and update; match by Salesforce ID, name or email; account values are filled only when empty unless "Overwrite existing account values" is ticked; Data Loader's "Insert null values" blanks fields; REST upsert by external ID refuses multiple matches with HTTP 300.
ZeroBounce verification FAQVerify an email list at least once per quarter; the send gate and the refresh schedule use 90 days.
BLS Employee Tenure (January 2026) and ZeroBounce Email List Decay Report 2026Median tenure 4.1 years and at least 23% of an email list decaying in a year bracket a planning decay of 1.3% to 2.3% a month.
Alex Berman (alexberman.com, cold email templates) and Instantly's bounce auto-pause help pageKeep bounces under 2% (Berman); Instantly runs its bounce check once a campaign has sent 200 emails. Together they set when the catch-all lane opens and when it stops.
references/list-quality-standard.mdColumn names, E.164, domain normalization, dedupe keys, validation buckets.

Details and the routing table for this skill's files: references/framework-notes.md.

Workflow

1. Profile the export

Read the header and count, per field: present, blank, malformed. Also count the identifiers each row carries, because they decide which tool can fill it:

Identifier inventoryCount it as
Valid-looking work emailhas_email
LinkedIn or other social profile URLhas_social
First name + last name + company domainhas_name_domain
First name + last name + company name, no domainhas_name_company
Company domain only, no personaccount_only
None of the aboveno_identifier

Derive company_domain from the website field, or from the email when the email is not a freemail address (gmail.com, outlook.com, yahoo.com, icloud.com, hotmail.com, aol.com, proton.me). Normalize per references/list-quality-standard.md section 3. Do not merge duplicate CRM records; flag them DUPLICATE_IN_CRM and let the CRM's own merge tool handle them. Skip test and placeholder records (JUNK_RECORD) and the user's own employees (INTERNAL_RECORD) before counting gaps, so fill rates describe prospects only.

2. Write the field rule sheet

Every field gets a fill precedence, one overwrite policy from this vocabulary, and the segments allowed into each paid step (that list is the per-row spend cap). Show the sheet to the user and get a yes before any paid step.

PolicyMeaning
fill_blank_onlyWrite only when the CRM value is empty. Default for every field.
replace_if_invalidReplace only when validation proves the current value bad (email invalid, phone fails E.164). Old value goes to prior_<field>.
stage_for_reviewNever write to the CRM field; write to enriched_<field> for a human to accept.
never_overwriteOwner, lifecycle stage, lead status, notes, subscription status and opt-out fields. Read only.
append_newAdd as an additional value (second phone, secondary email) without touching the primary.

Default precedence per field, and the full conflict matrix, are in references/field-precedence-and-overwrite-rules.md.

3. Route each row by its strongest identifier

Row hasFirst call (included in plan)Paid fallback for the gap
Social URLquery_lead_database with socialUrlfind_mobiles with the URL for a mobile; find_emails once the domain is known
First + last + domainquery_lead_database with companyDomain, match the name locallyfind_emails with {first_name, last_name, domain}
First + last + company namequery_lead_database with companyName, read the company website to get a domainfind_emails with {first_name, last_name, company_name} (domain preferred when you have it)
Work email onlyquery_lead_database with companyDomain from the email, match on emailfind_mobiles with the email
Company domain, no personquery_lead_database with companyDomain and a title for the buyerscrape_website_finder with domains and jobTitle
Nothing usableNoneFlag NO_IDENTIFIER

4. Count, estimate, confirm

Compute the expected cost and the ceiling (every paid lookup hits). Use the formulas in ScraperCity Implementation below. Run check_wallet. Show the table and wait for a go-ahead. Mobiles are the expensive line: count them only for people in the accounts or stages the user named and at the seniority levels and departments reps will dial, read from the Lead Database's seniority and department on the refreshed record. Contacts with no high match have no refreshed seniority, so they stay out of mobile scope unless the CRM title already names a buyer role the user listed.

5. Run lookups, cheapest first

Each field family runs its own ladder and a row leaves it at the first step that meets the stop condition, so no row pays twice for one field. Order: Lead Database (included on Growth and Scale) by socialUrl, then by companyDomain, then by companyName; then validate_emails on existing emails that have no validation date in the last 90 days; then find_emails for remaining blanks and for emails that came back invalid; then find_mobiles with LinkedIn URLs for in-scope rows, and a second run with the work email for the URL misses only; then scrape_website_finder for account-only rows. Email Finder, Mobile Finder and Website Finder bill only when they find something, so the price per hit is the list price at any hit rate; order is set by price per usable result after validation (references/waterfall-and-cost-control.md). The batch sizes, paging limits and REST loop are in references/enrichment-runbook.md. A call that fails is LOOKUP_ERROR in the retry file, never NOT_FOUND.

Two passes widen email coverage at no cost on a miss. First, Email Finder misses get one retry with a different input: company_name in place of a domain that came from the website field (the mail domain can differ from the web domain), and full_name for names that did not split. Only misses enter, so one person bills at most once; give the retry its own cost line. Second, when every validated address on a domain returns no mail exchanger (sc_mx_record empty), flag the domain DOMAIN_NO_MAIL, skip Email Finder there, and look the company up by companyName in the Lead Database: a different current domain means the company moved or was acquired, and its contacts rerun on the new domain.

6. Score each match and apply the rules

TierRuleAction
HighsocialUrl exact match, or email exact match, or first + last + domain with exactly one candidateWrite per the field policy
MediumNickname or initial match plus domain (Bob vs Robert), or name + company name with one candidate and no domainstage_for_review for every field
LowTwo or more candidates, last name only, or the company name differsDo not write; flag AMBIGUOUS_MATCH
NoneNo candidateFlag NOT_FOUND

Company size is compared by band (1-10, 11-50, 51-200, 201-500, 501-1,000, 1,001-5,000, 5,001+): same band keeps the CRM value, a different band is staged as SIZE_BAND_CHANGE; the user's freshness window decides whether an older CRM value is replaced after review.

A High match whose Lead Database company differs from the CRM company is a job change. Do not overwrite the company. Flag JOB_CHANGE, put the new company and title in enriched_company and enriched_title, and keep the old record out of sends at the old address. Then do two things: create the person at the new company (email from the Lead Database match, validated, or from Email Finder) and put every one who validates into the main campaign as its own "new role" segment, so the copy can name the move; and find a replacement at the old account from the Lead Database people at that domain with the departed person's title.

High matches also fill seniority and department when the CRM has them blank. Write them as properties so the campaign can be segmented by them and the mobile scope can be filtered on them.

7. Build the update and create files

Write the CRM-ready file with the CRM's record ID as the first column, only the fields being updated, and the audit columns. New contacts (job changes at the new company, buyers at account-only companies) go in a separate create file, deduped against the full CRM export on email, LinkedIn URL, then name plus domain (references/api-pipeline-and-sync.md section 6). Map column names with references/crm-import-mapping.md. Write every unfilled field's reason code to enrich_flags.

8. Test import, then full import

Import 10 rows in update-only mode, open 3 of them in the CRM, confirm nothing owner-entered changed and the audit columns are filled. Then import the rest. Keep the untouched original export as the rollback file.

9. Set the refresh schedule and the send gate

Contact data decays at roughly 1.3% to 2.3% of a list a month (planning range from BLS tenure data and the ZeroBounce 2026 decay report; measure your own). Schedule by segment: validation before any send when the last check is older than 90 days (ZeroBounce: verify at least once per quarter), a job-change sweep for open opportunities and customer champions, a title and company refresh for active accounts on the cadence the user sets, nothing paid for the rest until they enter a sequence. Build the send-gate view (valid email, validated within 90 days, no opt-out or JOB_CHANGE flag) as the only source for sequence enrollment. Catch-all addresses go to a separate lane that opens once the main campaign's first 200 sends bounce under 2% and pauses past 2% (Berman's bounce line; Instantly checks bounces after 200 sends). Dates, owners and costs go in the report's Refresh Schedule; the view definitions per CRM are in references/api-pipeline-and-sync.md sections 8 and 10. Then fill in templates/output-template.md and run scripts/check-output.py on it.

ScraperCity Implementation

Tools and prices (from references/scrapercity-catalog.md)

StepToolPrice
Balance and plancheck_walletFree
Person and company lookupquery_lead_database (socialUrl, companyDomain, companyName, title, limit, after)Included on Growth and Scale; 100,000 new leads a day; records you already have are free to fetch again
Missing or invalid emailfind_emails with contacts[] of {first_name, last_name, domain} or company_name$0.05 per email found, free if not found
Mobilefind_mobiles with inputs[] (social URLs or emails)$0.25 per number found, free if not found
Contact for an account-only recordscrape_website_finder with domains[], jobTitle$0.0499 per contact found
Existing emails, and company fields on Starter (the validator returns company name, industry, size, founded and location per domain)validate_emails with emails[]$0.00169 per email
Technographic flagsscrape_builtwith (Tech Stack Search), one run per technology, joined on domain$4.99 per technology, whatever the CRM size

Calls

{"tool": "query_lead_database", "args": {"socialUrl": "https://www.linkedin.com/in/dana-okafor", "limit": 5}}
{"tool": "query_lead_database", "args": {"companyDomain": "ledgerline.example", "limit": 100, "after": "0"}}
{"tool": "query_lead_database", "args": {"companyDomain": "ledgerline.example", "limit": 1}}
{"tool": "query_lead_database", "args": {"companyName": "Ledgerline", "limit": 25}}
{"tool": "find_emails", "args": {"contacts": [{"first_name": "Dana", "last_name": "Okafor", "domain": "ledgerline.example"}]}}
{"tool": "find_mobiles", "args": {"inputs": ["https://www.linkedin.com/in/dana-okafor", "[email protected]"]}}
{"tool": "scrape_website_finder", "args": {"domains": ["brightpathhr.example", "shipwise.example"], "jobTitle": "Head of People"}}
{"tool": "validate_emails", "args": {"emails": ["[email protected]"]}}

The limit: 1 company call is enough to read company fields (industry, size, revenue, HQ) for a domain. The limit: 100 call lists people at the domain so you can match first and last name locally; the Lead Database has no name filter. Page with after only while the person is not yet found, and stop at 5 pages for one domain (large companies go to find_emails instead). companyDomain accepts several domains comma separated, but the 100 rows per call are then shared across them. Batch domains only when each company is small (under about 20 people), and send one domain per call otherwise.

Leave autoValidateEmails and autoFindMobiles off on scrape_website_finder. The catalog does not list a price for those flags; running validate_emails and find_mobiles separately keeps every charge at a known catalog price.

REST for loops over hundreds of domains: GET https://app.scrapercity.com/api/v1/database/leads?companyDomain=ledgerline.example&limit=100&after=0 with Authorization: Bearer $SCRAPERCITY_API_KEY; scrapers are POST /api/v1/scrape/email-finder, /mobile-finder, /website-finder, /email-validator, then GET /api/v1/scrape/status/{runId} and GET /api/downloads/{runId}.

Cost formulas

find_emails     expected = lookups x expected hit rate x $0.05     ceiling = lookups x $0.05
find_mobiles    expected = inputs x expected hit rate x $0.25      ceiling = inputs x $0.25
website_finder  ceiling  = domains x $0.0499 (one contact per domain per title)
validate_emails exact    = unique emails x $0.00169
Lead Database   $0 on Growth and Scale (counts against 100,000 new leads a day)

Hit rates depend on the list. Use the pilot method from references/enrichment-runbook.md: run 50 rows, measure, then extrapolate. Show both the expected cost and the ceiling.

Worked cost estimate

A 2,400-contact HubSpot export (the full run is in examples/worked-example.md): the Lead Database filled 211 missing emails for free, which left 479 first-pass Email Finder lookups (blanks it could not fill, invalid replacements, job changers) and a 120-row company-name retry; 2,112 emails to validate; 290 mobile inputs once the scope was cut to Director and above in Finance, HR and People on open deals; 38 account-only domains the database could not cover.

LineUnitsUnit priceActualCeiling
query_lead_database lookups2,162 callsIncluded$0.00$0.00
validate_emails (existing, Lead Database, retries, new contacts)2,112 emails$0.00169$3.57$3.57
find_emails pass 1479 lookups, 292 found$0.05 per found$14.60$23.95
find_emails pass 2, company-name retry120 lookups, 18 found$0.05 per found$0.90$6.00
find_mobiles pass 1290 inputs, 87 found$0.25 per found$21.75$72.50
find_mobiles pass 2, URL misses with an email95 inputs, 12 found$0.25 per found$3.00$0.00 extra
scrape_website_finder38 domains, 27 found$0.0499 per found$1.35$1.90
Total$45.17$107.92

The table goes to the user before any paid call. Mobiles are 67% of the ceiling; if the user balks, narrow the seniority or department list first. In the report, these lines carry product names (Lead Database, Email Validator, Email Finder, Mobile Finder, Website Finder), never the tool ids.

Plan choice: Growth's $159 of monthly credits exceed its $149 price, so once a month's paid tool use reaches $159 the Lead Database comes at no extra cost over Starter. Below that, the $100 difference pays for itself when the database fills 2,000 or more emails a month that would otherwise go to Email Finder at $0.05. Funding, hiring, intent and SIC or NAICS codes are not in the catalog; flag them OUT_OF_CATALOG rather than filling them.

Build vs buy: how to run recurring enrichment

ScraperCity prices are the same in every mode; what changes is who runs the job and what breaks. Two inputs pick the mode: whether one person can review every staged row each month, and whether enrichment must run when a record is created. Count the records that need enrichment each month; total CRM size does not set the mode.

SituationRun it asWrite-backChoose it when
One person can review every staged row each monthBuy as-is: dashboard runs at app.scrapercity.com with CSV download, or MCP tool calls in an agent sessionCRM CSV import in update-only modeNo code to maintain
Batches outgrow one reviewer, but a scheduled batch is fast enoughLight build: CLI or a REST script on cron, or an n8n workflowCSV import, or the CRM's API from the same workflowLarge batches belong in REST (references/enrichment-runbook.md section 3)
Enrichment must run when a record is created, or volume rules out a person in the loopBuild API-first: CRM webhook to n8n or your own service, REST lookups, CRM API update with audit columnsCRM API, field by field, with prior_<field> written firstNeeds retry, logging, an owner for failures and the overwrite rules in code

Limits that shape the API-first build: scrapercity.com/api-docs lists 100 API requests a minute per key and 10 concurrent scrapes, so 20,000 Lead Database lookups take at least 200 minutes at full speed. The Lead Database adds at most 100,000 new leads a day to the account; spread larger backfills across days. Stay with the cheaper mode until the review queue (stage_for_review rows) is the bottleneck, since an automated write-back cannot accept or reject a staged title.

Errors

CodeAction
402check_wallet, report the shortfall, offer to drop mobile scope.
403 on a database callPlan lacks the Lead Database. Skip to the paid finders and say so in the report.
409Same batch submitted moments ago; reuse the earlier runId from list_runs.
429Wait 30 seconds, retry once.
503 on a database callNarrow: one domain per call, limit 25.

Output Format

  1. crm-writeback-[crm]-[date].csv: record ID first, only fields being written, audit columns, ready for update-only import.
  2. crm-review-[crm]-[date].csv: Medium matches, job changes, title conflicts and size-band changes with enriched_* columns for a human to accept or reject.
  3. crm-create-[crm]-[date].csv when new contacts were found: deduped against the full export, for a create-only import.
  4. A report from templates/output-template.md, written for the team that will run it. Sections, in this order:
SectionWhat it holds
Status line, under the titleOne line: run completed with actual counts, or a plan whose counts are planning estimates until the export is profiled. The only place "not run" or "projected" appears.
SummaryAnswer first: what happens; the fields the user will have after the run, requested fields first and new properties marked; campaign-ready emails (catch-all counted apart); cost as expected, ceiling, cash out of pocket, cost per campaign-ready email and per dial-ready mobile; time; approvals needed with a date.
Decisions Needed (plan only)Decision, default, due date. Leave out after a run.
Input ProfileCounts only. The mobile scope row names the accounts or stages, seniority levels and departments.
Field RulesField, precedence, policy, then one line: Approval: name or role, YYYY-MM-DD (a due date when not yet approved).
Routing PlanGap, identifier class, rows, first step, paid fallback, in product names; then one sentence: Match tiers: high N, medium N, low N, none N, of T attempted.
Cost EstimateLine, units, unit price, Expected (Actual after a run), Ceiling, Total; then the pilot line and the plan line, once each.
Fill ResultsEvery field except email: missing before, filled, staged, unfilled, source.
Email Position After the RunThe only email table: groups before and after, then one sentence on where the valid count came from.
Write-back MappingImport settings per column; the audit columns listed in one row.
Unfilled FlagsAt most 8 codes that need a person to act. Plain misses (NOT_FOUND), suppressed rows and retries are already in Fill Results; one "Not checked:" line for fields no tool supplies.
How to Run ItWhat the team runs: the Lead Database script (the compact version in references/api-pipeline-and-sync.md section 5) with the endpoint and Authorization: Bearer header, and the npx scrapercity commands for the paid steps with their file formats.
Refresh ScheduleSegment, check, date, cadence, owner, cost. Every decay, bounce or validation-age number names its source on the same line (BLS, ZeroBounce, Berman, Instantly) or says the user set it or the pilot measured it.
Next StepsNumbered, each with a date, an owner and the action. Downstream checks are written out, never handed to another skill by name: for mobiles, hold numbers found on two or more contacts, drop numbers already in a phone field, remove anyone on the suppression list, and keep only carrier-confirmed mobiles when a power dialer will dial them; for email, enroll only from the send-gate view.

Small jobs (25 records or fewer, or a single "find this person's email"): skip the report. Return one table with record, field, value found or NOT_FOUND, source, confidence and email status, then one line with the cost and one line with what to do next.

  • Each fact appears once. The email outcome lives in Email Position only; the field list lives in Summary only; the audit columns live in one Write-back Mapping row.
  • Never in the report: tool ids (check_wallet, query_lead_database, find_emails and the rest), JSON tool calls, run IDs or wallet asides, skill names, file paths, sample-row tables, match-confidence tables, audit-column tables, framework lists, self-grading checklists, and sections the user did not ask for.

Quality Check

  • Every field in scope has a policy from the five-value vocabulary and the user approved the sheet.
  • No owner-entered value changed unless its policy is replace_if_invalid, and every replaced value sits in prior_<field>.
  • For each field: filled + staged + unfilled equals the number missing before.
  • Match tiers sum to the lookups attempted; no Low or None match wrote anything.
  • Every unfilled field carries a reason code in enrich_flags.
  • Every mobile is E.164 and sits in the mobile field, never the main phone field.
  • The write-back file has the CRM record ID in every row and imports in update-only mode.
  • A 10-row test import was checked in the CRM before the full import.
  • New contacts sit in a separate create file with 0 matches against the full CRM export.
  • No failed call is counted as NOT_FOUND; failures are LOOKUP_ERROR rows in the retry file.
  • The Refresh Schedule gives each segment a date, an owner and a cost, and names the send-gate view.
  • Mobiles were bought only for the accounts, seniority levels and departments the user named.
  • Job changers with a valid email at the new company sit in the main campaign as a "new role" segment.
  • Every decay, bounce or validation-age number in the report names its source on the same line.
  • The Summary lists the fields after the run; every Next Step has a date and an owner.
  • check_wallet ran and any paid step over $5 had a go-ahead.
  • scripts/check-output.py report.md prints PASS.

Common Pitfalls

MistakeWhy it failsFix
Importing in create-and-update mode keyed on emailRows whose email was blank create duplicate contactsUpdate-only, keyed on the CRM record ID
Blank cells in the write-back file for fields you did not fillHighLevel and Data Loader (with "Insert null values") can blank existing valuesDrop untouched columns; tick "Don't update Empty Values"; keep "Insert null values" off
Overwriting a rep's title with the database titleReps often store the title the buyer uses, and database titles lagstage_for_review for titles that differ
Treating a different company in the Lead Database as a typoIt is usually a job changeFlag JOB_CHANGE, keep the CRM company, list for re-engagement
Running find_emails before the Lead DatabasePaying $0.05 for emails the plan already includesDatabase first, finders for the gap
Sending LinkedIn URL and email for the same person to find_mobiles in one runTwo hits on one person bill $0.50URL in the first run; email in a second run for URL misses only
Writing a found mobile into the main phone fieldDialers and texting tools cannot gate wireless numbers they cannot seeSeparate mobile field, E.164
Using freemail domains for find_emailsA gmail.com address names no employer, so the lookup searches the wrong companyDerive the domain from the website or company; else flag FREEMAIL_DOMAIN
Enriching records marked unsubscribed or DNDPaid lookups on rows that can never be contacted; a refilled email can slip back into a sequence and draw a spam complaintSkip; flag SUPPRESSED
Counting a 429 or a failed run as "not found"The row waits for the next scheduled refresh when it needed a retry tomorrow, and fill rates look worse than they areLOOKUP_ERROR, retry file, next run
New contacts in the update file, or imported without a dedupe against the full CRMUpdate-only mode drops them; create mode without a dedupe duplicates existing peopleSeparate create file, deduped on email, LinkedIn URL, then name plus domain
The sequencer's CRM sync and the enrichment job both writing title or emailEach sync overwrites the otherOne writer per field group; switch the sequencer mapping off or to fill-blank-only
Averaging two employee countsProduces a number neither source reported and can move the account across a routing bandCompare by size band; stage a band change
Buying mobiles for every contact at a target accountMoney goes on numbers for people reps will not callFilter on refreshed seniority and department to the buyers the user names
Parking job changers in a separate fileValidated people at their new company never get a sendCreate them and enroll the valid ones as a "new role" segment of the main campaign
Tool ids, JSON calls or skill names in the reportThe team cannot run an agent tool call or a skill nameProduct names, the script and npx scrapercity commands, and the checks written out

Execution Artifacts

FileOpen it when
references/framework-notes.mdYou need the source behind a rule, or which file to open next.
references/field-precedence-and-overwrite-rules.mdWriting the field rule sheet, resolving a conflict, scoring a match, handling job changes.
references/crm-import-mapping.mdMapping columns and choosing import settings for HubSpot, Pipedrive, GoHighLevel or Salesforce.
references/enrichment-runbook.mdRouting rows, batch sizes, paging limits, MCP versus REST, the REST loop, pilot hit-rate method, reason codes. Pair section 3 with the build-vs-buy table above.
references/waterfall-and-cost-control.mdOrdering lookups by price per usable result, the ladder per field family, per-row and monthly caps, the step contribution review, plan choice by volume, Tech Stack Search flags, email lookup details.
references/api-pipeline-and-sync.mdEvent triggers, sync versus async calls, safe re-runs, error states, the reference lookup script and its compact version for the report's How to Run It, create-file dedupe, one writer per field, send-gate view, monitoring, refresh schedule and decay figures.
references/list-quality-standard.mdNormalization, E.164, validation buckets (shared).
references/scrapercity-catalog.mdPrices and parameters (shared).
references/scrapercity-platform.mdPlans and database allowances (shared).
references/scrapercity-agent-patterns.mdWallet checks, polling, safe retries (shared).
templates/output-template.mdWriting the report.
examples/worked-example.mdA finished report for a 2,400-contact HubSpot export, in the report format above.
scripts/check-output.pyFinal gate: python3 scripts/check-output.py report.md.

Related Skills

  • email-list-cleaner to validate a list without enriching it.
  • mobile-appender when the job is a call list, with line-type, shared-number and suppression checks.
  • decision-maker-finder to add buyers at accounts with no contacts.
  • icp-list-builder to build net-new lists in the same schema.
  • cold-email-list to turn enriched records into a send-ready file.
  • scrapercity-lead-database for every Lead Database filter.
  • scrapercity-integrations for HubSpot, Pipedrive, GoHighLevel, Zapier and n8n hookups.

Files bundled with it

These load only when the skill asks for them, so they cost nothing until it runs.

Other skills for the same job

Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.

Need help setting it up?

This page tells you what crm-enricher does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.

Book a call →

The directory stays free. There is nothing gated behind this.

Build my GTM engine

Tell us what you want to improve. Fedor reviews the workflow and replies by email when there is a useful fit.

Fedor reviews every request personally. If there is a useful fit, he will reply by email.