teachyou.ai academy
← All posts
n8n

n8n for Lead Generation: Scraping, Enriching and Scoring Leads

Ira Menon · Jun 7, 2026 · 18 min read

Why lead generation is the perfect n8n use case

Most sales teams still run lead generation like it's 2015: a growth hire opens a spreadsheet, manually searches LinkedIn or a directory site, copy-pastes company names into a doc, then spends an afternoon looking up emails one by one. By the time the list is "ready," half the data is stale and nobody has scored which leads are actually worth a rep's time.

The reason n8n fits this problem so well is that lead generation is really a pipeline of small, mechanical decisions strung together: find a source of candidate leads, pull structured data out of it, cross-reference that data against two or three enrichment APIs, apply a scoring rule, and route the result somewhere a human or another system can act on it. None of those steps require creativity. All of them require consistency, and consistency is exactly what workflow automation delivers and manual research doesn't.

This article walks through building a real n8n lead generation workflow: scraping a source (a directory site, a search results page, or a public dataset), enriching each lead with company and contact data, scoring leads against your ideal customer profile, and pushing qualified leads into a CRM or spreadsheet with zero manual copy-pasting. We'll cover the actual node configuration, the HTTP Request patterns you need for scraping and enrichment APIs, the Code node logic for scoring, and the operational details — rate limits, deduplication, error handling — that separate a demo workflow from one that runs reliably every day.

If you're new to n8n's node model, expressions, or trigger types, it's worth getting comfortable with the basics before tackling a multi-stage pipeline like this — the concepts compound quickly once you understand how data flows between nodes.

Mapping the lead generation pipeline before you build anything

Before opening the n8n canvas, sketch the pipeline as a sequence of questions, because each question maps directly to a stage in the workflow:

  • Where do candidate leads come from? A public directory, a job board, a review site like G2 or Capterra, a CSV export from an event, or a search engine results page.
  • What raw signal do you extract at the source? Usually a company name, a domain, a job title, or a person's name — rarely a full enriched profile.
  • What do you need to add to make the lead usable? Company size, industry, funding stage, tech stack, a verified email, a LinkedIn URL.
  • What makes a lead "qualified" for your specific offer? This is your scoring rubric, and it has to be defined in business terms before you write a single line of scoring logic.
  • Where does a qualified lead need to end up, and what does an unqualified lead do instead? Usually a CRM (HubSpot, Pipedrive, Airtable) for qualified leads and a "nurture later" tag or sheet for everyone else.

Writing this out matters because the temptation with n8n is to start dragging nodes onto the canvas immediately. Workflows built that way tend to have scoring logic bolted onto whatever fields happened to be easy to scrape, rather than scraping and enrichment steps chosen because they produce the fields the scoring model actually needs. Decide the scoring criteria first, then work backward to figure out what data you need to collect.

For this walkthrough, assume a realistic B2B SaaS scenario: you sell a tool to mid-market e-commerce companies, your ICP is companies with 20-200 employees using Shopify or a comparable platform, and you want a daily workflow that finds new candidate companies, enriches them, scores them, and drops anything scoring above a threshold into a CRM pipeline stage called "New — Auto-Sourced."

Stage one: scraping candidate leads with HTTP Request and HTML Extract

n8n doesn't ship a dedicated "scraper" node, and that's a feature, not a gap — scraping in n8n is really just an HTTP Request node followed by an HTML Extract node (or a call to a scraping API if the target site blocks direct requests), and that combination is flexible enough to handle almost any source.

A simple, low-risk starting point is a public company directory or a "built with Shopify" style listing page. The pattern looks like this:

  1. Schedule Trigger — run once a day, early morning, so enrichment API calls don't collide with anyone else's automation and results are ready before the sales team's morning stand-up.
  2. HTTP Request node — GET the directory listing page. Set a realistic User-Agent header, and if the site paginates, use an expression on the URL (?page={{ $itemIndex + 1 }}) combined with a Split In Batches or a simple loop to walk through several pages.
  3. HTML Extract node — point it at the CSS selectors for the fields on the page: company name, website link, maybe a short description. n8n's HTML Extract node lets you define multiple extraction rules in one node, each producing a field on the output item.
  4. Item List / Split Out node — because the extract node usually returns one item containing an array of results, split that array into individual items so each company becomes its own item flowing through the rest of the workflow.

If the target site renders content with JavaScript and a plain HTTP Request returns an empty shell, you have three realistic options: use a headless-browser scraping API (many exist specifically for this and return clean HTML or JSON over a normal REST call n8n can hit with HTTP Request), use a site's own public search or sitemap endpoints if they exist, or fall back to an official API when the site provides one — always prefer the API route when it's available, since it's more stable than scraping and won't break when the site redesigns its markup.

A code-node deduplication step belongs right after extraction, because directory pages often repeat entries across pages or across daily runs:

// Deduplicate by normalized domain
const seen = new Set();
const output = [];

for (const item of $input.all()) {
  const domain = (item.json.website || '')
    .toLowerCase()
    .replace(/^https?:\/\//, '')
    .replace(/^www\./, '')
    .replace(/\/$/, '');

  if (!domain || seen.has(domain)) continue;
  seen.add(domain);
  output.push({ json: { ...item.json, domain } });
}

return output;

This single normalization step — always work off a cleaned domain, never a raw URL string — will save you from double-processing the same lead under https://acme.com, http://www.acme.com/, and acme.com as three separate rows.

Stage two: enriching leads with firmographic and contact data

Raw scraped data is almost never enough to score a lead. You typically need three categories of enrichment: firmographic (company size, industry, funding), technographic (what software the company uses), and contact-level (a name and email for the actual person you'd reach out to).

In n8n, enrichment is just a chain of HTTP Request nodes against whichever data providers you have accounts with, feeding their responses back into the item as new fields. The exact providers vary by budget and region, but the workflow shape is identical regardless of vendor:

  1. HTTP Request to a company enrichment API — send the cleaned domain, get back employee count, industry, and location. Store the raw response in a field like enrichment_firmographic so you keep the full payload for later debugging, and also pull out the two or three fields you actually need into top-level fields for scoring.
  2. IF node — does the domain resolve to a real, enrichable company? Some scraped rows will be defunct sites, personal blogs, or duplicates the enrichment API can't match. Route failures to a separate "needs manual review" output rather than silently dropping them or letting a null value poison your scoring math downstream.
  3. HTTP Request to a technographic API (optional but valuable for ICP fit) — check whether the company's site uses the platform your product integrates with. This is often the single strongest signal for a niche B2B tool, stronger than company size alone.
  4. HTTP Request to a contact-finder API — given the domain and optionally a target job title ("Head of Ecommerce," "VP Marketing"), retrieve a name, title, and email for the best-fit contact at that company.

A pattern worth calling out explicitly: use the Merge node to fan enrichment calls out and back in, rather than chaining them strictly serially when they don't depend on each other. If your firmographic lookup and technographic lookup both only need the domain, run them in parallel branches and merge the results, instead of waiting on one API call to finish before starting the next. This alone can cut a four-stage enrichment chain's runtime by more than half, since API latency — not your workflow logic — is usually the bottleneck.

Here's a Code node pattern for flattening nested enrichment responses into flat, scoring-ready fields, which keeps your downstream IF and Code nodes far more readable than reaching into nested JSON everywhere:

const results = [];

for (const item of $input.all()) {
  const firmo = item.json.enrichment_firmographic || {};
  const tech = item.json.enrichment_technographic || {};
  const contact = item.json.enrichment_contact || {};

  results.push({
    json: {
      domain: item.json.domain,
      company_name: firmo.name || item.json.company_name || '',
      employee_count: firmo.employees || null,
      industry: firmo.industry || '',
      uses_target_platform: Boolean(tech.platforms?.includes('shopify')),
      contact_name: contact.full_name || '',
      contact_title: contact.title || '',
      contact_email: contact.email || '',
      email_verified: contact.verification_status === 'valid',
    },
  });
}

return results;

Always add rate limiting and retry handling around enrichment calls. Most providers throttle aggressively, and a 429 response mid-run shouldn't kill the whole workflow. Set the HTTP Request node's built-in retry option (2-3 retries with a backoff), and if a provider has a hard per-minute cap, insert a Wait node or use Split In Batches with a small batch size so you're not firing fifty simultaneous requests at an API that allows ten per second.

Stage three: scoring leads against your ideal customer profile

Scoring is where the workflow earns its keep, because this is the step that turns "a list of companies" into "a prioritized list a rep should actually work today." Resist the urge to make scoring subjective or vague ("looks like a good fit") — write it as an explicit points-based rubric a Code node can execute deterministically.

A workable rubric for the e-commerce SaaS example:

  • Company size fit (0-30 points): full points for 20-200 employees, partial credit tapering off outside that band, zero for companies under 5 or over 1000 employees.
  • Platform fit (0-30 points): full points if the technographic check confirmed the target platform, zero otherwise — for many products this single signal should dominate the score, since it's the closest thing to a "can this company even use our product" gate.
  • Contact quality (0-20 points): full points if you have a verified email for a decision-maker-level title, partial credit for an unverified email or a lower-seniority title, zero if no contact was found at all.
  • Industry fit (0-20 points): full points for industries in your target list, partial credit for adjacent industries, zero for clear mismatches.
function scoreLead(lead) {
  let score = 0;

  // Company size fit
  const size = lead.employee_count || 0;
  if (size >= 20 && size <= 200) score += 30;
  else if (size >= 10 && size < 20) score += 15;
  else if (size > 200 && size <= 500) score += 15;

  // Platform fit
  if (lead.uses_target_platform) score += 30;

  // Contact quality
  const seniorTitles = ['vp', 'head', 'director', 'chief', 'founder', 'owner'];
  const titleMatch = seniorTitles.some(t =>
    (lead.contact_title || '').toLowerCase().includes(t)
  );
  if (lead.contact_email && lead.email_verified && titleMatch) score += 20;
  else if (lead.contact_email && lead.email_verified) score += 12;
  else if (lead.contact_email) score += 5;

  // Industry fit
  const targetIndustries = ['e-commerce', 'retail', 'consumer goods'];
  const industry = (lead.industry || '').toLowerCase();
  if (targetIndustries.some(i => industry.includes(i))) score += 20;
  else if (industry.includes('marketing') || industry.includes('agency')) score += 8;

  return score;
}

return $input.all().map(item => ({
  json: {
    ...item.json,
    lead_score: scoreLead(item.json),
  },
}));

The reason to build scoring as a single deterministic function rather than a chain of IF nodes is maintainability — when a sales leader tells you next quarter that company size should count for less and technographic fit for more, you change four numbers in one Code node instead of rewiring a dozen branches on the canvas.

Once you have a numeric score, add a Switch node downstream that routes leads into three buckets: hot (say, 70+), warm (40-69), and cold (below 40). Each bucket gets a different downstream action, which is the next stage.

Stage four: routing leads into your CRM and notification tools

A scored lead that just sits in n8n's execution log has produced zero business value. The routing stage is what makes the pipeline useful day to day:

  • Hot leads go straight into your CRM (HubSpot, Pipedrive, Airtable, or even a Google Sheet if you're pre-CRM) as new records in a specific pipeline stage, tagged with the source and the score, and trigger a Slack or email notification to the rep or team responsible so nothing sits unseen for a day.
  • Warm leads go into a nurture list — often just a tagged segment in your email tool or CRM — for a slower-cadence outbound sequence rather than an immediate rep touch.
  • Cold leads get logged (a simple spreadsheet row is fine) for record-keeping and deduplication purposes but don't trigger any outbound action. You still want them recorded so tomorrow's run doesn't rediscover and rescan the same company.

A CRM push in n8n typically looks like this, using an HTTP Request node against the CRM's API when there's no dedicated integration node, or the CRM's native n8n node when one exists:

// Example: build the payload for a CRM "create contact" API call
return $input.all().map(item => ({
  json: {
    properties: {
      company: item.json.company_name,
      domain: item.json.domain,
      email: item.json.contact_email,
      jobtitle: item.json.contact_title,
      lead_score: item.json.lead_score,
      lead_source: 'n8n_auto_sourced',
      lifecyclestage: 'lead',
    },
  },
}));

Before writing to the CRM, always check for an existing record on the same domain or email first — a lookup call followed by an IF node ("record exists? update : create") prevents the workflow from spawning duplicate contacts every single day it runs. This is one of the most common mistakes in a first-pass lead gen workflow: it works beautifully for a week, then someone notices the CRM has the same fifteen companies logged nine times each.

For the Slack notification on hot leads, keep the message dense with the information a rep needs to act in the next thirty seconds — company name, score, contact name and title, and a direct link to the CRM record — rather than a generic "new lead added" ping that forces the rep to click through before they know whether it's worth their time.

Handling errors, rate limits, and flaky sources without breaking the pipeline

A lead gen workflow runs unattended, usually on a schedule, which means it has to survive bad days from any of its external dependencies without a human noticing until morning. A few practices make the difference between a workflow that silently produces garbage and one you can trust:

  • Wrap risky HTTP Request nodes in error handling. Use the node's "Continue on Fail" setting (or an Error Trigger workflow) so a single failed enrichment call for one lead doesn't halt the entire batch of fifty leads behind it.
  • Log failures to a visible place, not just n8n's internal execution history. A simple append to a Google Sheet or a message to a low-priority Slack channel listing which leads failed enrichment and why means you can spot a pattern (an API key expiring, a provider changing its response schema) within a day instead of a month.
  • Respect provider rate limits explicitly rather than discovering them through 429 errors. Check documented limits, then size your Split In Batches windows and Wait node delays to stay comfortably under them — comfortably, not exactly at the limit, because latency spikes on the provider's end will otherwise push you over.
  • Version your scoring logic. Once the scoring Code node is live and leads are flowing into a CRM, treat changes to the rubric like a schema migration — tag scored leads with which rubric version produced their score so you can explain, three months from now, why two leads that look similar today ended up with very different scores back then.
  • Set a maximum items-per-run ceiling. If a source site changes its pagination and your workflow suddenly tries to scrape 3,000 rows instead of 30, you want a hard cap that stops the run and alerts you, rather than an enrichment bill for three thousand API calls overnight.

None of these are exotic n8n features — they're Continue on Fail toggles, IF nodes, and a Sheet or Slack node you already know how to use. The discipline is in remembering to add them before the workflow has been running unattended for a month, not after a bad run costs you an enrichment budget or fills your CRM with duplicates.

Extending the workflow: multi-source aggregation and lead re-scoring

Once the single-source version is stable, the natural next step is aggregating multiple lead sources into one pipeline — a directory site, a job board (companies hiring for roles related to your product are often a strong buying-intent signal), and a review site — all feeding into the same dedup, enrichment, and scoring stages via a Merge node upstream of the deduplication Code node.

Another high-value extension is re-scoring existing leads on a schedule, not just scoring new ones. A lead that scored 45 points three weeks ago might now show new buying-intent signals — a job posting for a relevant role, a funding announcement, a new integration listed on their site — and a weekly re-scoring pass that pulls fresh technographic and firmographic data for leads already in your "warm" bucket can catch leads that have become sales-ready without anyone manually re-checking the list.

You can also add a simple intent layer: an HTTP Request to a news or job-posting API filtered by company domain, checked for keywords relevant to your product category, adding bonus points to the scoring function when a company shows recent hiring or funding activity in your space. This is the same Code-node scoring pattern from Stage Three — just with an additional input field and an additional rule in the point calculation.

Common mistakes to avoid when building this in n8n

A few patterns show up repeatedly in lead-gen workflows that don't hold up past the first week:

  • Scraping too aggressively from a single source. Sending dozens of concurrent requests to one directory site will get your IP rate-limited or blocked outright. Throttle with Split In Batches and add short Wait steps between page requests.
  • Scoring on incomplete data without accounting for it. If your contact-finder API fails for a lead, that lead's contact-quality score defaults to zero in the code above — which is correct — but make sure your scoring function treats "missing data" differently from "data that failed the fit criteria" if that distinction matters for your rubric, otherwise you'll systematically underscore leads where enrichment simply timed out.
  • No deduplication across runs, only within a run. Deduplicating scraped items within a single execution isn't enough — you need a persistent store (a Sheet, a database table, or your CRM itself) that the workflow checks against every run so the same company isn't re-scraped, re-enriched, and re-scored (burning API credits each time) day after day.
  • Treating the scoring rubric as fixed forever. Revisit it against actual sales outcomes. If leads scoring 80+ are closing at the same rate as leads scoring 50, your weights are wrong, and that's a data problem the workflow can't fix on its own — it just faithfully executes whatever rubric you gave it.
  • Skipping a human review step entirely. Full automation from scrape to CRM is achievable, but keeping a "needs review" bucket for medium-confidence leads (ambiguous enrichment matches, borderline scores) for a few weeks after launch will surface rubric problems faster than trusting the pipeline blindly from day one.

Bringing it together

A working n8n lead generation pipeline is really four disciplined stages stacked on top of each other: a scraping stage that turns a public source into structured items, an enrichment stage that adds the firmographic, technographic, and contact data your scoring model needs, a scoring stage that applies an explicit, tunable rubric instead of gut feel, and a routing stage that gets qualified leads in front of a rep before the opportunity goes cold. Every one of those stages is built from ordinary n8n nodes — HTTP Request, HTML Extract, Code, IF, Switch, Merge — there's no proprietary lead-gen node doing anything you couldn't inspect and modify yourself.

The payoff is a system that runs every morning without anyone opening a spreadsheet, that gets more accurate over time as you tune the scoring rubric against real sales outcomes, and that frees your sales team to spend their time on conversations instead of research. Start with one source and a simple three-factor rubric, get it running reliably for two weeks, then layer in additional sources, an intent signal, and a re-scoring pass once you trust the foundation.

If you want to go deeper on building agentic, decision-making workflows on top of n8n — not just fixed pipelines but flows where an LLM node decides how to route or personalize outreach based on the enriched lead data — that's exactly what we cover in the n8n AI Agent Tutorial course on teachyou.ai, where we build multi-step automations that combine n8n's node ecosystem with AI reasoning steps.