AutomationFlowsMarketing & Ads › Generate Scored B2b Leads From Google Maps Websites to Google Sheets

Generate Scored B2b Leads From Google Maps Websites to Google Sheets

ByJannik Hiller @jannik-mtm on n8n.io

This n8n workflow is a sophisticated B2B Lead Generation Scraper. It automates the entire journey from discovering businesses on Google Maps to extracting, scoring, and saving high-quality contact emails.

Event trigger★★★★★ complexity30 nodesHTTP RequestGoogle Sheets
Marketing & Ads Trigger: Event Nodes: 30 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #11924 — we link there as the canonical source.

This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "id": "41259296-ec07-4167-a1c2-d7307fc4499c",
      "name": "Loop over queries",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        912,
        688
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "67ca1049-b28e-41ec-bb86-d23ab4bec479",
      "name": "Search Google Maps with query",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "maxTries": 2,
      "position": [
        1232,
        704
      ],
      "parameters": {
        "url": "=https://places.googleapis.com/v1/places:searchText",
        "method": "POST",
        "options": {
          "allowUnauthorizedCerts": false
        },
        "jsonBody": "={{ { \"textQuery\": $json.query || $('Loop over queries').item.json.query, ...($json.pageToken ? { \"pageToken\": $json.pageToken } : {}) } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-Goog-FieldMask",
              "value": "places.displayName,places.formattedAddress,places.rating,places.nationalPhoneNumber,places.regularOpeningHours,places.websiteUri,places.businessStatus,nextPageToken"
            }
          ]
        }
      },
      "executeOnce": false,
      "retryOnFail": true,
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "0cddbb70-a8eb-4e97-8ebd-101e745517e4",
      "name": "Run workflow",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        688,
        688
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "473f6344-a1bc-4721-a9f4-4ffee8a5bc5f",
      "name": "Extract Places Data",
      "type": "n8n-nodes-base.set",
      "position": [
        2112,
        752
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "id-1",
              "name": "name",
              "type": "string",
              "value": "={{ $json.places.displayName.text }}"
            },
            {
              "id": "id-2",
              "name": "address",
              "type": "string",
              "value": "={{ $json.places.formattedAddress }}"
            },
            {
              "id": "id-3",
              "name": "rating",
              "type": "number",
              "value": "={{ $json.places.rating }}"
            },
            {
              "id": "id-4",
              "name": "phone",
              "type": "string",
              "value": "={{ $json.places.nationalPhoneNumber }}"
            },
            {
              "id": "id-5",
              "name": "openingHours",
              "type": "object",
              "value": "={{ $json.places.regularOpeningHours }}"
            },
            {
              "id": "id-6",
              "name": "website",
              "type": "string",
              "value": "={{ $json.places.websiteUri }}"
            },
            {
              "id": "id-7",
              "name": "businessStatus",
              "type": "string",
              "value": "={{ $json.places.businessStatus }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "ea0dbc01-474d-42ee-917a-b13a533d4b6b",
      "name": "Check if Website Exists",
      "type": "n8n-nodes-base.if",
      "position": [
        2736,
        944
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.places.websiteUri }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c861ccaa-2318-46ee-b830-4e6035624c8d",
      "name": "Check for Next Page Token",
      "type": "n8n-nodes-base.code",
      "position": [
        1552,
        688
      ],
      "parameters": {
        "jsCode": "// Get the response from the Google Maps API\nconst response = $input.first().json;\n\n// Get the query from the input\nconst query = $input.first().json.query || $('Search Google Maps with query').first().json.query;\n\n// Extract nextPageToken if it exists\nconst nextPageToken = response.nextPageToken || null;\n\n// Extract places array from the response\nconst places = response.places || [];\n\n// Initialize or get accumulated places from execution custom data\nif (!$execution.customData.accumulatedPlaces) {\n  $execution.customData.accumulatedPlaces = [];\n}\n\n// Accumulate the places\n$execution.customData.accumulatedPlaces = $execution.customData.accumulatedPlaces.concat(places);\n\n// Return the nextPageToken, query, and accumulated places\nreturn [\n  {\n    json: {\n      query: query,\n      nextPageToken: nextPageToken,\n      pageToken: nextPageToken,\n      places: $execution.customData.accumulatedPlaces,\n      currentPagePlaces: places.length,\n      totalAccumulatedPlaces: $execution.customData.accumulatedPlaces.length\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "f2929d01-3781-4ba8-a759-5f0754000c6a",
      "name": "Has Next Page?",
      "type": "n8n-nodes-base.if",
      "position": [
        1856,
        736
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.nextPageToken }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "70c3a947-403f-4698-989d-a2f376c39ac6",
      "name": "Merge Emails with Business Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        6128,
        640
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "2ff082fb-27f6-4e6e-919c-e5a24f570ec7",
      "name": "Merge All Businesses",
      "type": "n8n-nodes-base.merge",
      "position": [
        6352,
        960
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "185fa4e8-5162-4d8a-80e6-9e62e6d7a8b0",
      "name": "Split Out Places",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        2352,
        752
      ],
      "parameters": {
        "include": "allOtherFields",
        "options": {},
        "fieldToSplitOut": "places"
      },
      "typeVersion": 1
    },
    {
      "id": "9c0020e6-dd29-49bf-b801-71794fe40386",
      "name": "Filter Operational Businesses",
      "type": "n8n-nodes-base.filter",
      "position": [
        2592,
        752
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.places.businessStatus }}",
              "rightValue": "OPERATIONAL"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d599c3a4-28f0-4a65-bdec-1664952e088f",
      "name": "Fetch Website HTML",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        2928,
        816
      ],
      "parameters": {
        "url": "={{ $json.places.websiteUri }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "0d0a8bf0-b3b3-413a-8f7a-7ee4b0a4db9f",
      "name": "Extract Emails from HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        4384,
        784
      ],
      "parameters": {
        "jsCode": "// 1. Identify the field containing the HTML (Mapped here)\nconst html = $input.item.json.combinedHtml || $input.item.json.data || $input.item.json.body || $input.item.json.html || '';\n\n// 2. Clone the business data (Name, Phone, etc.)\nconst businessData = { ...$input.item.json };\n\n// 3. Clean up business data so we don't pass the HTML text to Google Sheets\ndelete businessData.data;\ndelete businessData.body;\ndelete businessData.html;\ndelete businessData.error;\ndelete businessData.combinedHtml;\n\n// 4. Extract Emails\nconst emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g;\nconst emailMatches = html.match(emailRegex) || [];\n\n// 5. Remove duplicates and filter out image-based emails\nconst uniqueEmails = [...new Set(emailMatches)].filter(email => {\n  return !/\\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(email);\n});\n\n// 6. Return the data\nif (uniqueEmails.length === 0) {\n  // If no email found, return the business info with a blank email field\n  return { json: { ...businessData, email: '' } };\n} \n\n// If multiple emails found, n8n allows you to return an array \n// of objects, which will automatically split into separate rows.\nreturn uniqueEmails.map(email => ({\n  json: { ...businessData, email: email }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "f326a102-87c7-4c22-a476-0b0f720605cb",
      "name": "Filter Valid Emails",
      "type": "n8n-nodes-base.filter",
      "position": [
        4640,
        784
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "notRegex"
              },
              "leftValue": "={{ $json.email }}",
              "rightValue": "(google|gstatic|ggpht|schema\\.org|example\\.com|png|jpg|gif|jpeg|wixpress|sentry|imli|noreply|no-reply|donotreply|mailer-daemon|postmaster)"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "79fc536e-709a-492a-b494-86dd62d4085a",
      "name": "Append to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        6912,
        960
      ],
      "parameters": {
        "columns": {
          "value": {
            "name": "={{ $json.places.displayName.text }}",
            "email": "={{ $json.email }}",
            "phone": "={{ $json.places.nationalPhoneNumber }}",
            "places": "={{ $json.places.displayName.languageCode }}",
            "address": "={{ $json.places.formattedAddress }}",
            "website": "={{ $json.places.websiteUri }}",
            "businessStatus": "={{ $json.places.businessStatus }}"
          },
          "schema": [
            {
              "id": "name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "address",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rating",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "website",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "website",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "businessStatus",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "businessStatus",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "places",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "places",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 509043379,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pBTh42nVAp3X1OTgTsdWVAc-YUosIPI4vGRilqWJMYY/edit#gid=509043379",
          "cachedResultName": "B2B Lead Scraper"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1pBTh42nVAp3X1OTgTsdWVAc-YUosIPI4vGRilqWJMYY",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1pBTh42nVAp3X1OTgTsdWVAc-YUosIPI4vGRilqWJMYY/edit?usp=drivesdk",
          "cachedResultName": "Solar leads"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "6ee08a6f-f4ee-40d0-87a1-8b42d82705e8",
      "name": "Extract Contact Links",
      "type": "n8n-nodes-base.code",
      "position": [
        3216,
        880
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Get the HTML from the data field\nconst html = $input.item.json.data || '';\n\n// Clone the business data to preserve it\nconst businessData = { ...$input.item.json };\n\n// Extract the base URL from the website URI\nconst websiteUri = businessData.places?.websiteUri || '';\nlet baseUrl = '';\ntry {\n  const urlObj = new URL(websiteUri);\n  baseUrl = `${urlObj.protocol}//${urlObj.hostname}`;\n} catch (e) {\n  baseUrl = websiteUri;\n}\n\n// Use regex to find all href attributes in anchor tags\nconst hrefRegex = /href=[\"']([^\"']+)[\"']/gi;\nconst matches = [];\nlet match;\n\nwhile ((match = hrefRegex.exec(html)) !== null) {\n  matches.push(match[1]);\n}\n\n// Filter for contact-related links with expanded keywords\nconst contactKeywords = ['contact', 'about', 'reach', 'info', 'get-in-touch', 'touch', 'connect', 'team', 'privacy', 'terms', 'legal', 'imprint', 'impressum'];\nconst contactLinks = matches.filter(link => {\n  const lowerLink = link.toLowerCase();\n  return contactKeywords.some(keyword => lowerLink.includes(keyword));\n});\n\n// Convert all relative URLs to absolute URLs\nconst absoluteContactLinks = contactLinks.map(link => {\n  if (link.startsWith('/')) {\n    return baseUrl + link;\n  } else if (!link.startsWith('http')) {\n    return baseUrl + '/' + link;\n  }\n  return link;\n});\n\n// Return all matching contact links as separate items\nif (absoluteContactLinks.length === 0) {\n  // If no contact links found, return business data with empty contactLink\n  return {\n    json: {\n      ...businessData,\n      contactLink: ''\n    }\n  };\n}\n\n// Return an array of items, one for each contact link\nreturn absoluteContactLinks.map(contactLink => ({\n  json: {\n    ...businessData,\n    contactLink: contactLink\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "305994a9-dc4b-4174-af52-b39037b1b02d",
      "name": "Filter Contact Links",
      "type": "n8n-nodes-base.filter",
      "position": [
        3472,
        880
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "id-1",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.contactLink }}"
            },
            {
              "id": "b6925dc5-3140-4e6d-943b-3654c3bd8203",
              "operator": {
                "type": "string",
                "operation": "notRegex"
              },
              "leftValue": "={{ $json.contactLink }}",
              "rightValue": "\\.(png|jpg|jpeg|ico|css|svg)$"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a5bb802b-e446-454d-b823-376bab92b602",
      "name": "Fetch Contact Page HTML",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        3728,
        880
      ],
      "parameters": {
        "url": "={{ $json.contactLink }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "allowUnauthorizedCerts": true
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "3fc0737f-9453-44ba-8cb7-1ea6f77716b1",
      "name": "Remove Duplicate Businesses",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        6640,
        960
      ],
      "parameters": {
        "compare": "selectedFields",
        "options": {},
        "fieldsToCompare": "places.displayName.text"
      },
      "typeVersion": 2
    },
    {
      "id": "9bff479f-b55f-4f78-877d-cbfa2de9ba40",
      "name": "Combine Homepage and Contact HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        4032,
        784
      ],
      "parameters": {
        "jsCode": "// Get all input items\nconst items = $input.all();\n\nif (items.length === 0) {\n  return [];\n}\n\n// Get the first item to preserve business data\nconst firstItem = items[0].json;\n\n// Extract and combine HTML from all items\nlet combinedHtml = '';\n\nfor (const item of items) {\n  const html = item.json.data || item.json.body || item.json.html || '';\n  if (html) {\n    combinedHtml += html + '\\n\\n';\n  }\n}\n\n// Clone business data from first item\nconst businessData = { ...firstItem };\n\n// Clean up unnecessary fields\ndelete businessData.data;\ndelete businessData.body;\ndelete businessData.html;\ndelete businessData.error;\ndelete businessData.contactLink;\n\n// Return combined data\nreturn {\n  json: {\n    ...businessData,\n    combinedHtml: combinedHtml.trim()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "340da66a-cf7b-4aae-aa4e-e199bca9b4a1",
      "name": "Remove Duplicate Emails",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        5872,
        784
      ],
      "parameters": {
        "compare": "selectedFields",
        "options": {},
        "fieldsToCompare": "email"
      },
      "typeVersion": 2
    },
    {
      "id": "b510e4cb-fd35-42de-9d71-0732454402b6",
      "name": "Score and Rank Emails",
      "type": "n8n-nodes-base.code",
      "position": [
        5168,
        784
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Get the email and business data\nconst email = $input.item.json.email || '';\nconst website = $input.item.json.website || $input.item.json.places?.websiteUri || '';\nconst businessData = { ...$input.item.json };\n\n// Initialize quality score\nlet qualityScore = 100;\n\n// Extract email parts\nconst emailLower = email.toLowerCase();\nconst emailLocalPart = emailLower.split('@')[0];\nconst emailDomain = emailLower.split('@')[1] || '';\n\n// Extract domain from business website\nlet businessDomain = '';\nif (website) {\n  try {\n    const urlObj = new URL(website);\n    businessDomain = urlObj.hostname.replace('www.', '');\n  } catch (e) {\n    businessDomain = website.replace(/^(https?:\\/\\/)?(www\\.)?/, '').split('/')[0];\n  }\n}\n\n// Penalize generic email prefixes (heavy penalty)\nconst genericPrefixes = ['info', 'contact', 'admin', 'support', 'sales', 'hello', 'noreply', 'no-reply', 'enquiry', 'enquiries', 'inquiry', 'inquiries', 'office', 'general', 'help', 'service', 'team', 'mail'];\nif (genericPrefixes.some(prefix => emailLocalPart === prefix || emailLocalPart.startsWith(prefix + '.'))) {\n  qualityScore -= 40;\n}\n\n// Reward emails that appear to have personal names (contains dots or multiple parts)\nif (emailLocalPart.includes('.') && !genericPrefixes.some(prefix => emailLocalPart.includes(prefix))) {\n  qualityScore += 20;\n}\n\n// Check domain match with business website\nif (businessDomain && emailDomain) {\n  if (emailDomain === businessDomain || emailDomain.includes(businessDomain) || businessDomain.includes(emailDomain)) {\n    qualityScore += 30; // Reward matching domain\n  } else {\n    qualityScore -= 20; // Penalize non-matching domain\n  }\n}\n\n// Penalize free email providers\nconst freeProviders = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 'mail.com', 'protonmail.com'];\nif (freeProviders.includes(emailDomain)) {\n  qualityScore -= 25;\n}\n\n// Ensure score stays within 0-100 range\nqualityScore = Math.max(0, Math.min(100, qualityScore));\n\n// Return the business data with quality score\nreturn {\n  json: {\n    ...businessData,\n    emailQualityScore: qualityScore\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "01be1b17-7fc3-48e5-b8c7-82ec4c543fea",
      "name": "Sort by Email Quality",
      "type": "n8n-nodes-base.sort",
      "position": [
        5424,
        784
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "order": "descending",
              "fieldName": "emailQualityScore"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "60c2b837-1b12-4202-8a88-18fc57777fb4",
      "name": "Keep Best Email Only",
      "type": "n8n-nodes-base.limit",
      "position": [
        5648,
        784
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "b941abc6-cfb5-4698-8396-bdb057a0e8c3",
      "name": "Group by Business",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        4896,
        784
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "31691192-c241-447e-9aad-83dedad59d9e",
      "name": "\ud83d\udccb Overview: B2B Lead Generation Scraper",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        -384
      ],
      "parameters": {
        "color": "yellow",
        "width": 352,
        "height": 768,
        "content": "## B2B Lead Generation Scraper\n\nThis workflow automates lead discovery and qualification from Google Maps. It searches for businesses, scrapes their websites for contact emails, scores email quality, and exports verified leads to Google Sheets.\n\n### How it works\n1. Input search queries (e.g., \"Dentist New York\", \"Plumber London\")\n2. Searches Google Maps for all matching businesses\n3. Filters for operational businesses only\n4. Visits business websites to find contact emails\n5. Scores emails by quality (domain match, personal names, generic prefixes)\n6. Keeps the best email per business\n7. Exports deduplicated leads to Google Sheets\n\n### Setup steps\n1. Get Google Places API key from Google Cloud Console\n2. Enable Places API (New) in your project\n3. Add credentials to \"Search Google Maps with query\" node\n4. Connect Google Sheets credentials for lead export\n5. Prepare search queries in JSON format (see Query Prompt sticky)\n6. Adjust email scoring thresholds if needed"
      },
      "typeVersion": 1
    },
    {
      "id": "9c7113ab-8389-4266-a74e-269ef72e2937",
      "name": "Stage 1: Google Maps Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 832,
        "height": 640,
        "content": "## Stage 1: Google Maps Search\n\nLoops through search queries, fetches all matching businesses from Google Maps, handles pagination automatically, and filters for operational businesses only."
      },
      "typeVersion": 1
    },
    {
      "id": "bf4a9c62-21c2-4f0a-87c5-927d46f43c7a",
      "name": "Stage 2: Website Scraping",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1488,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 2784,
        "height": 640,
        "content": "## Stage 2: Website Scraping\n\nVisits business websites and contact pages to extract all available email addresses. Handles both homepage and dedicated contact page scraping."
      },
      "typeVersion": 1
    },
    {
      "id": "fadc94fd-fc55-4ae7-8978-418d2c743578",
      "name": "Stage 3: Email Quality Control",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4272,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 1744,
        "height": 640,
        "content": "## Stage 3: Email Quality Control\n\nScores emails by quality (+30 domain match, +20 personal names, -40 generic prefixes, -25 free providers). Keeps only the best email per business."
      },
      "typeVersion": 1
    },
    {
      "id": "3cb35ee5-bff7-487e-8fb7-4c6d3d54bac9",
      "name": "Stage 4: Final Output",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6000,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 1104,
        "height": 640,
        "content": "## Stage 4: Final Output & Deduplication\n\nMerges email data with business info, removes duplicate businesses, and exports clean leads to Google Sheets."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Run workflow": {
      "main": [
        [
          {
            "node": "Loop over queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Next Page?": {
      "main": [
        [
          {
            "node": "Search Google Maps with query",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract Places Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out Places": {
      "main": [
        [
          {
            "node": "Filter Operational Businesses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group by Business": {
      "main": [
        [
          {
            "node": "Score and Rank Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop over queries": {
      "main": [
        [],
        [
          {
            "node": "Search Google Maps with query",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Website HTML": {
      "main": [
        [
          {
            "node": "Extract Contact Links",
            "type": "main",
            "index": 0
          },
          {
            "node": "Combine Homepage and Contact HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Places Data": {
      "main": [
        [
          {
            "node": "Split Out Places",
            "type": "main",
            "index": 0
          },
          {
            "node": "Loop over queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Valid Emails": {
      "main": [
        [
          {
            "node": "Group by Business",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Contact Links": {
      "main": [
        [
          {
            "node": "Fetch Contact Page HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep Best Email Only": {
      "main": [
        [
          {
            "node": "Remove Duplicate Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Businesses": {
      "main": [
        [
          {
            "node": "Remove Duplicate Businesses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Contact Links": {
      "main": [
        [
          {
            "node": "Filter Contact Links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score and Rank Emails": {
      "main": [
        [
          {
            "node": "Sort by Email Quality",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort by Email Quality": {
      "main": [
        [
          {
            "node": "Keep Best Email Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Google Sheets": {
      "main": [
        [
          {
            "node": "Loop over queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Website Exists": {
      "main": [
        [
          {
            "node": "Fetch Website HTML",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge All Businesses",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Fetch Contact Page HTML": {
      "main": [
        [
          {
            "node": "Combine Homepage and Contact HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicate Emails": {
      "main": [
        [
          {
            "node": "Merge Emails with Business Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Extract Emails from HTML": {
      "main": [
        [
          {
            "node": "Filter Valid Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Next Page Token": {
      "main": [
        [
          {
            "node": "Has Next Page?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicate Businesses": {
      "main": [
        [
          {
            "node": "Append to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Operational Businesses": {
      "main": [
        [
          {
            "node": "Check if Website Exists",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Emails with Business Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google Maps with query": {
      "main": [
        [
          {
            "node": "Check for Next Page Token",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop over queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Emails with Business Data": {
      "main": [
        [
          {
            "node": "Merge All Businesses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Homepage and Contact HTML": {
      "main": [
        [
          {
            "node": "Extract Emails from HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This n8n workflow is a sophisticated B2B Lead Generation Scraper. It automates the entire journey from discovering businesses on Google Maps to extracting, scoring, and saving high-quality contact emails.

Source: https://n8n.io/workflows/11924/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

This n8n workflow automates the process of finding ecommerce seller leads, enriching them with product and business details, discovering company websites, and extracting contact information such as em

Google Sheets, N8N Nodes Mrscraper, HTTP Request
Marketing & Ads

This template is for B2B sales teams, SDRs, growth marketers, and founders who maintain a spreadsheet of prospects and need verified contact details -- emails and mobile numbers -- without manual rese

Google Sheets, HTTP Request
Marketing & Ads

This workflow finds local businesses from Google Maps and automatically enriches them with emails, social profiles, AI summaries, and personalized outreach messages — all saved to Google Sheets. Searc

HTTP Request, Google Sheets
Marketing & Ads

This workflow leverages n8n to perform automated Google Maps API queries and manage data efficiently in Google Sheets. It's designed to extract specific location data based on a given list of ZIP code

Execute Workflow Trigger, Stop And Error, HTTP Request +1
Marketing & Ads

This repository contains an SLA-based lead routing workflow built in n8n, designed to ensure fast lead response, fair sales distribution, and controlled escalation without relying on a full CRM system

Form Trigger, Google Sheets, Slack +1