
Lead Enrichment
Take a raw form submission, resolve the company behind the email domain, score it against your ICP, and write a deduplicated record into the CRM.
n8n · Apollo.io · Supabase · HubSpot · Docker
Download Workflow JSON: Import straight into any n8n instance, self-hosted or cloud.
Deploy to n8n Cloud: If you would rather not own the box. The workflow file is identical either way.
Copy docker-compose.yml: Postgres rather than the default SQLite. SQLite survives a demo and corrupts under concurrent executions.
Signups arrive with an email address and nothing else. Someone opens LinkedIn, works out whether the company is worth a call, and types what they found into the CRM. By the time that happens the lead is a day old and the record is inconsistent with every other record.
Everything this blueprint calls. Check you have them before you start - the usual reason a build stalls halfway is a key that takes a day to get approved.
This runs comfortably on the smallest shared-vCPU box. Self-hosting is what makes the per-lead cost effectively zero.
Free tier: From about $4.50 a month
Resolves a domain into headcount, industry, and funding. Any enrichment provider works - the node is isolated behind one HTTP call.
Free tier: Limited monthly credits
Caches enrichment by domain. The second signup from the same company costs nothing, which matters more than it sounds once you are paying per credit.
Free tier: Two projects, 500MB each
HubSpot in the file. Any CRM that can match on a unique property will do.
Some links above are partner links and we earn a commission if you sign up through them. It costs you nothing extra, and it does not decide what goes in a blueprint - the self-hosted option is recommended wherever it is genuinely the better call.
Monthly, at the volumes this blueprint was tested against. Worth comparing against what the manual version of this work costs you in hours.
| Component | Cost | At what volume |
|---|---|---|
| Hetzner CX22 | $4.59/mo | Runs n8n, Postgres, and Caddy with room left |
| Apollo credits | ~$0.03/lead | Cached by domain, so repeat companies are free |
| Supabase | $0 | Free tier, a single cache table |
The files
The workflow imports in one paste. The compose file and Caddy config are the part that usually costs an afternoon, and both are included.
lead-enrichment.workflow.json - import straight into n8n
docker-compose.yml with Postgres, plus a hardened .env template
Caddyfile for TLS on a bare VPS
The Supabase cache table as SQL, with the domain index
The decisions that matter, in the order you will meet them. Everything here is a thing that broke in testing before it was a rule.
Respond 202 the moment the payload validates, then do the work. Enrichment takes two to four seconds and the form in front of it should never wait on that. n8n does this with a Respond to Webhook node placed before the enrichment branch.
Free mail domains, role addresses, and anything failing a syntax check stop here and go to the CRM unenriched. Sending gmail.com to an enrichment API burns a credit to be told it is Google.
const FREE_MAIL = new Set(['gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'icloud.com']);
const ROLE = /^(info|support|sales|admin|hello|contact|billing)@/i;
const email = $json.email.trim().toLowerCase();
const domain = email.split('@')[1] ?? '';
return [{
json: {
email,
domain,
enrichable: Boolean(domain) && !FREE_MAIL.has(domain) && !ROLE.test(email),
},
}];Look the domain up in Supabase first. Enrichment data is stable for weeks, so a 30-day cache typically removes 40 to 60 percent of calls once the same companies start signing up twice.
One Function node holding the ICP rules beats nine chained IF nodes. The scoring logic changes most often, and a single readable function is something you can diff and a colleague can review.
Match on company domain and update in place. Insert-only is how a CRM ends up with the same account four times under four spellings, and no enrichment pipeline survives that for long.
The difference between a demo and something you can leave running is entirely in this table. Every row is a failure the blueprint handles explicitly rather than hoping about.
| Failure | What happens |
|---|---|
| Apollo times out | Two retries, then the lead is written unenriched and flagged for a nightly backfill run. |
| Malformed webhook body | Rejected 400 at the validation node. The raw body is logged so a broken form is visible immediately. |
| CRM rate limit | Queued in Postgres and retried with backoff. The webhook has already answered, so nothing upstream notices. |
| Duplicate submission | Email plus a 60-second window checked against the cache table, so a double-click produces one record. |
The record written to the CRM.
{
"email": "dana@northwind-logistics.com",
"domain": "northwind-logistics.com",
"company": "Northwind Logistics",
"headcount": 240,
"industry": "Transportation & Logistics",
"country": "US",
"icp_score": 82,
"tier": "A",
"enriched_from": "cache",
"enriched_at": "2026-08-21T14:02:11Z"
}Done-For-You
The workflow assumes a clean HubSpot. Most CRMs we meet have custom properties, a dedupe history, and a sales team with opinions about tiering.
A TechZapp sprint is a fixed-scope, fixed-price week. A senior engineer builds it in your environment, hands over the repository and the runbook, and you own every part of it afterwards. No platform of ours to keep paying for.
Deployed on your infrastructure with TLS, backups, and monitoring
ICP scoring built from your closed-won data rather than from a guess
Mapped to your CRM properties, with a dry-run against a copy before it writes
Sprint pricing
One sprint, typically 5 working days
Fixed scope agreed before we start
Built in your environment, not ours
Repository, infrastructure, and runbook handed over
Two weeks of support after handover included
Scope This SprintTell us what you are integrating with. We reply within one business day, and we will say plainly if the blueprint above already covers it.