AutomationFlowsMarketing & Ads › Google Maps Lead Generation with Email Discovery

Google Maps Lead Generation with Email Discovery

Google Maps Lead Generation with Email Discovery. Uses @hasdata/n8n-nodes-hasdata, googleSheets, formTrigger. Event-driven trigger; 25 nodes.

Event trigger★★★★☆ complexity25 nodes@Hasdata/N8N Nodes HasdataGoogle SheetsForm Trigger
Marketing & Ads Trigger: Event Nodes: 25 Complexity: ★★★★☆ Added:

This workflow follows the Form Trigger → Google Sheets 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
{
  "name": "Google Maps Lead Generation with Email Discovery",
  "nodes": [
    {
      "parameters": {
        "resource": "google_maps",
        "q": "={{ $json.queries.leads_query }}",
        "additionalFields": {
          "start": "={{ $json.queries.pageOffset }}",
          "ll": "@40.685676,-74.009913,14z"
        }
      },
      "id": "56d672ee-fe20-4218-973a-6de94a122460",
      "name": "Search Google Maps",
      "type": "@hasdata/n8n-nodes-hasdata.hasData",
      "typeVersion": 1,
      "position": [
        -1072,
        112
      ],
      "credentials": {
        "hasDataApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const emailNotFound = 'Not found';\n\nconst PLACEHOLDER_EMAILS = new Set([\n  'user@domain.com', 'name@domain.com', 'email@domain.com',\n  'user@example.com', 'name@example.com', 'email@example.com',\n  'info@example.com', 'test@test.com', 'admin@example.com',\n  'your@email.com', 'email@website.com', 'name@yoursite.com',\n  'email@email.com', 'name@company.com', 'yourname@domain.com',\n  'username@domain.com', 'your@domain.com', 'you@example.com',\n]);\n\nconst FAKE_DOMAINS = new Set([\n  'example.com', 'domain.com', 'website.com', 'email.com',\n  'test.com', 'yoursite.com', 'yourdomain.com', 'company.com',\n  'sentry.io', 'wixpress.com', 'placeholder.com',\n]);\n\nconst INVALID_EXTENSIONS = [\n  '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp',\n  '.ico', '.css', '.js', '.json', '.xml',\n];\n\nconst NOREPLY_PREFIXES = [\n  'noreply', 'no-reply', 'mailer-daemon', 'postmaster',\n  'donotreply', 'do-not-reply',\n];\n\nfunction isValidCompanyEmail(email, websiteUrl) {\n  const lower = email.toLowerCase();\n  if (PLACEHOLDER_EMAILS.has(lower)) return false;\n\n  const parts = lower.split('@');\n  if (parts.length !== 2) return false;\n  const [local, emailDomain] = parts;\n\n  if (FAKE_DOMAINS.has(emailDomain)) return false;\n  if (/example|test|sample|demo|fake|placeholder/.test(emailDomain)) return false;\n  if (NOREPLY_PREFIXES.some(p => local === p)) return false;\n\n  return true;\n}\n\nfunction domainMatchesWebsite(email, websiteUrl) {\n  if (!websiteUrl) return false;\n  try {\n    const emailDomain = email.toLowerCase().split('@')[1];\n    const siteDomain = websiteUrl\n      .replace(/^https?:\\/\\//, '')\n      .replace(/^www\\./, '')\n      .split('/')[0].toLowerCase();\n    return emailDomain === siteDomain ||\n      siteDomain.endsWith('.' + emailDomain) ||\n      emailDomain.endsWith('.' + siteDomain);\n  } catch (e) { return false; }\n}\n\nfunction extractAllEmails(html) {\n  if (!html) return [];\n  html = html\n    .replace(/\\s*\\[at\\]\\s*|\\s*\\(at\\)\\s*/gi, '@')\n    .replace(/\\s*\\[dot\\]\\s*|\\s*\\(dot\\)\\s*/gi, '.');\n\n  const regex = /\\b[a-zA-Z0-9._%+-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}\\b/g;\n  const matches = html.match(regex);\n  if (!matches) return [];\n\n  const valid = [];\n  for (const email of matches) {\n    const lower = email.toLowerCase();\n    if (INVALID_EXTENSIONS.some(ext => lower.endsWith(ext))) continue;\n    if (/-\\d+x\\d+/i.test(lower)) continue;\n    valid.push(lower);\n  }\n  return [...new Set(valid)];\n}\n\nconst zipped = $input.all().map((x, i) => [x, $('Is there a website?').all()[i]]);\nlet results = [];\nfor (const [webScrapeItem, mapsScrapeItem] of zipped) {\n  const content = webScrapeItem.json.content;\n  const locationData = mapsScrapeItem.json.locationResults;\n  const website = locationData.website || '';\n\n  let allEmails = extractAllEmails(content)\n    .filter(e => isValidCompanyEmail(e, website));\n\n  // Sort: domain-matching emails first\n  allEmails.sort((a, b) => {\n    const aMatch = domainMatchesWebsite(a, website) ? 0 : 1;\n    const bMatch = domainMatchesWebsite(b, website) ? 0 : 1;\n    return aMatch - bMatch;\n  });\n\n  const email = allEmails.length > 0 ? allEmails.join(', ') : emailNotFound;\n  results.push({ json: { locationResults: locationData, email: email } });\n}\nreturn results;\n"
      },
      "id": "bbd00c82-6599-41c3-bea0-0e23bb272594",
      "name": "Extract Email from Website",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "leftValue": "={{ $json.email }}",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "rightValue": "Not found",
              "id": "68d704b2-147e-4792-8091-85acb6180186"
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "96109f96-c25e-42f3-aaf3-f16f495f58ad",
      "name": "Email Found?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "q": "={{ $json.locationResults.title }} email",
        "additionalFields": {}
      },
      "id": "3a969281-2648-4981-8840-d98183929629",
      "name": "Search Google for Email",
      "type": "@hasdata/n8n-nodes-hasdata.hasData",
      "typeVersion": 1,
      "position": [
        240,
        240
      ],
      "credentials": {
        "hasDataApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const PLACEHOLDER_EMAILS = new Set([\n  'user@domain.com', 'name@domain.com', 'email@domain.com',\n  'user@example.com', 'name@example.com', 'email@example.com',\n  'info@example.com', 'test@test.com', 'admin@example.com',\n  'your@email.com', 'email@website.com', 'name@yoursite.com',\n  'email@email.com', 'name@company.com', 'yourname@domain.com',\n  'username@domain.com', 'your@domain.com', 'you@example.com',\n]);\n\nconst FAKE_DOMAINS = new Set([\n  'example.com', 'domain.com', 'website.com', 'email.com',\n  'test.com', 'yoursite.com', 'yourdomain.com', 'company.com',\n  'sentry.io', 'wixpress.com', 'placeholder.com',\n]);\n\nconst INVALID_EXTENSIONS = [\n  '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp',\n  '.ico', '.css', '.js', '.json', '.xml',\n];\n\nconst NOREPLY_PREFIXES = [\n  'noreply', 'no-reply', 'mailer-daemon', 'postmaster',\n  'donotreply', 'do-not-reply',\n];\n\nfunction isValidCompanyEmail(email, websiteUrl) {\n  const lower = email.toLowerCase();\n  if (PLACEHOLDER_EMAILS.has(lower)) return false;\n\n  const parts = lower.split('@');\n  if (parts.length !== 2) return false;\n  const [local, emailDomain] = parts;\n\n  if (FAKE_DOMAINS.has(emailDomain)) return false;\n  if (/example|test|sample|demo|fake|placeholder/.test(emailDomain)) return false;\n  if (NOREPLY_PREFIXES.some(p => local === p)) return false;\n\n  return true;\n}\n\nfunction extractAllEmails(html) {\n  if (!html) return [];\n  html = html\n    .replace(/\\s*\\[at\\]\\s*|\\s*\\(at\\)\\s*/gi, '@')\n    .replace(/\\s*\\[dot\\]\\s*|\\s*\\(dot\\)\\s*/gi, '.');\n\n  const regex = /\\b[a-zA-Z0-9._%+-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}\\b/g;\n  const matches = html.match(regex);\n  if (!matches) return [];\n\n  const valid = [];\n  for (const email of matches) {\n    const lower = email.toLowerCase();\n    if (INVALID_EXTENSIONS.some(ext => lower.endsWith(ext))) continue;\n    if (/-\\d+x\\d+/i.test(lower)) continue;\n    valid.push(lower);\n  }\n  return [...new Set(valid)];\n}\n\nconst zipped = $input.all().map((x, i) => [x, $('Merge1').all()[i]]);\nfor (const [serpItem, mapsData] of zipped) {\n  const serpData = serpItem.json.organicResults ?? [];\n  const website = mapsData.json.locationResults.website || '';\n  mapsData.json.email = 'Not found';\n\n  let foundEmails = [];\n  for (const data of serpData.slice(0, 5)) {\n    const snippet = data.snippet ?? '';\n    const emails = extractAllEmails(snippet)\n      .filter(e => isValidCompanyEmail(e, website));\n    foundEmails.push(...emails);\n  }\n\n  foundEmails = [...new Set(foundEmails)];\n  if (foundEmails.length > 0) {\n    mapsData.json.email = foundEmails.join(', ');\n  }\n}\n\nreturn $('Merge1').all();\n"
      },
      "id": "4f884b38-6786-47c8-a228-5a917ff8c627",
      "name": "Extract Email from Google",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        240
      ]
    },
    {
      "parameters": {
        "resource": "spreadsheet",
        "title": "=Lead - {{ $now.toFormat(\"yyyy-MM-dd HH:mm\") }}",
        "sheetsUi": {
          "sheetValues": [
            {
              "title": "Leads"
            },
            {
              "title": "Runs"
            }
          ]
        },
        "options": {}
      },
      "id": "4f81c024-ef5d-44b7-ab0d-cdb1ac21ba09",
      "name": "Create Spreadsheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1488,
        16
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Create Spreadsheet').item.json.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Create Spreadsheet').item.json.sheets[0].properties.sheetId }}",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "id": "74871695-cadc-4ddc-9fe6-58160f859403",
      "name": "Save Leads to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1936,
        16
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        800,
        112
      ],
      "id": "d9567831-5d9a-4b9e-947f-cc8a07cd8803",
      "name": "Merge"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        1024,
        112
      ],
      "id": "56e64a6c-7b64-4277-801c-52f2fbee1c74",
      "name": "Aggregate"
    },
    {
      "parameters": {
        "fieldToSplitOut": "queries",
        "include": "={{ $json.queries[0] }}",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        -1296,
        112
      ],
      "id": "0fe29978-5b43-407e-9259-fa15b86a29c4",
      "name": "Split Out1"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "include": "={{ $json.localResults }}",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        -832,
        112
      ],
      "id": "2db837a4-3392-4b18-9ffa-9a9aa8c38f48",
      "name": "Aggregate1"
    },
    {
      "parameters": {
        "jsCode": "const combined_localResults = [];\nconst seenTitles = new Set();\n\nfor (const data of $input.first().json.data) {\n  for (const result of (data.localResults || [])) {\n    const title = result.title;\n\n    if (!seenTitles.has(title)) {\n      seenTitles.add(title);\n\n      combined_localResults.push({\n        json: {\n          locationResults: result\n        }\n      });\n    }\n  }\n}\n\nreturn combined_localResults;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -576,
        112
      ],
      "id": "b94cb123-bb10-4c82-a86a-c4e5e5e7d776",
      "name": "Remove Duplicates"
    },
    {
      "parameters": {
        "jsCode": "const leads_query = $input.first().json['Search Locations Query'];\nconst defaultPageCount = Number($input.first().json['Pages per query to scrape'] ?? 0);\n\nconst outputQuery = [];\nfor (let i = 0; i < defaultPageCount; i++) {\n  outputQuery.push({ pageOffset: i * 20, leads_query });\n}\n\nreturn { json: { queries: outputQuery } };\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1520,
        112
      ],
      "id": "cfe54683-4ce4-40c6-95f7-f943eb235f09",
      "name": "Prepare Queries"
    },
    {
      "parameters": {
        "resource": "web_scraping",
        "url": "={{ $json.locationResults.website }}",
        "additionalFields": {}
      },
      "id": "ea935903-4c87-4326-89e8-de16ad6056a3",
      "name": "Scrape Websites",
      "type": "@hasdata/n8n-nodes-hasdata.hasData",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "alwaysOutputData": true,
      "credentials": {
        "hasDataApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "formTitle": "Form input",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Search Locations Query",
              "placeholder": "example: dentist in Brooklyn, NY (city/street/zip/neighborhood)"
            },
            {
              "fieldLabel": "Pages per query to scrape",
              "fieldType": "number",
              "defaultValue": "3"
            },
            {
              "fieldLabel": "Google Sheets Document Id",
              "placeholder": "Leave this empty if you want to create a new file"
            },
            {
              "fieldLabel": "Google Sheets ID",
              "placeholder": "Leave this empty if you want to create a new file"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.5,
      "position": [
        -1776,
        112
      ],
      "id": "6a6a1ae9-2dc2-4fc3-bc98-c4056b0ca470",
      "name": "On form submission"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "c4353e2a-28aa-4718-9f6a-3081a1401259",
              "leftValue": "={{ $json.locationResults.website }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -352,
        112
      ],
      "id": "8300b730-c77e-48cb-b678-8da9d75a8809",
      "name": "Is there a website?"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        0,
        240
      ],
      "id": "83ccc6b4-1481-4435-8bc5-c44f9dc62d72",
      "name": "Merge1"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "a4891843-7c4a-4c0e-b9e3-ea82cd748ba8",
              "leftValue": "={{ $('On form submission').item.json['Google Sheets Document Id'] }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            },
            {
              "id": "5937010c-ddbc-4887-b88e-1f164c66dd16",
              "leftValue": "={{ $('On form submission').item.json['Google Sheets ID'] }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1248,
        112
      ],
      "id": "6a896ad0-687b-4184-9df0-a00afc3f0343",
      "name": "Is google sheet empty"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $('On form submission').item.json['Google Sheets Document Id'] }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('On form submission').item.json['Google Sheets ID'] }}",
          "mode": "id"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "Title"
          ],
          "schema": [
            {
              "id": "Title",
              "displayName": "Title",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Email",
              "displayName": "Email",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Phone",
              "displayName": "Phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Rating",
              "displayName": "Rating",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Reviews",
              "displayName": "Reviews",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Website",
              "displayName": "Website",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Address",
              "displayName": "Address",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Latitude",
              "displayName": "Latitude",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Longitude",
              "displayName": "Longitude",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1712,
        208
      ],
      "id": "25598919-5580-4123-b92e-244153e1d4ca",
      "name": "Save leads to existing sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "let output = [];\nfor (const item of $('Aggregate').first().json.data) {\n  output.push({\n    Title: item.locationResults.title ?? \"\",\n    Email: item.email ?? \"\",\n    Phone: item.locationResults.phone ?? \"\",\n    Rating: item.locationResults.rating ?? \"\",\n    Reviews: item.locationResults.reviews ?? \"\",\n    Website: item.locationResults.website ?? \"\",\n    Address: item.locationResults.address ?? \"\",\n    Latitude: item.locationResults.gpsCoordinates.latitude ?? \"\",\n    Longitude: item.locationResults.gpsCoordinates.longitude ?? \"\",\n  });\n}\n\nreturn output;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1488,
        208
      ],
      "id": "639ec17d-d716-4222-816f-e367c690e6d5",
      "name": "Combine with old and remove duplicates"
    },
    {
      "parameters": {
        "jsCode": "let output = [];\nfor (const item of $('Aggregate').first().json.data) {\n  output.push({\n    Title: item.locationResults.title ?? \"\",\n    Email: item.email ?? \"\",\n    Phone: item.locationResults.phone ?? \"\",\n    Rating: item.locationResults.rating ?? \"\",\n    Reviews: item.locationResults.reviews ?? \"\",\n    Website: item.locationResults.website ?? \"\",\n    Address: item.locationResults.address ?? \"\",\n    Latitude: item.locationResults.gpsCoordinates.latitude ?? \"\",\n    Longitude: item.locationResults.gpsCoordinates.longitude ?? \"\",\n  });\n}\n\nreturn output;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1712,
        16
      ],
      "id": "de1b6236-6635-477e-a1ba-0a5014978d63",
      "name": "Structure data"
    },
    {
      "parameters": {
        "jsCode": "const form = $('On form submission').first().json;\nconst aggregated = $('Aggregate').first().json.data || [];\n\nfunction hasEmail(raw) {\n  if (raw === undefined || raw === null) return false;\n  const value = String(raw).trim().toLowerCase();\n  return value !== '' && value !== 'not found';\n}\n\nconst leadsFound = aggregated.length;\nconst leadsWithEmails = aggregated.filter((item) => hasEmail(item.email)).length;\n\nreturn [{\n  json: {\n    RunAtUTC: new Date().toISOString(),\n    SearchQuery: form['Search Locations Query'] ?? '',\n    PagesRequested: String(form['Pages per query to scrape'] ?? ''),\n    LeadsFound: String(leadsFound),\n    LeadsWithEmails: String(leadsWithEmails),\n    LeadsWithoutEmails: String(leadsFound - leadsWithEmails),\n    DocumentID: $('Create Spreadsheet').first().json.spreadsheetId ?? '',\n    LeadsSheetID: String($('Create Spreadsheet').first().json.sheets?.[0]?.properties?.sheetId ?? ''),\n    RunMode: 'new_sheet'\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2160,
        16
      ],
      "id": "5bd82a1b-7e44-4a0f-bc10-ebd96d25c84c",
      "name": "Prepare Run Log (New Sheet)"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Create Spreadsheet').item.json.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Runs",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [
            {
              "id": "RunAtUTC",
              "displayName": "RunAtUTC",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "SearchQuery",
              "displayName": "SearchQuery",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "PagesRequested",
              "displayName": "PagesRequested",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsFound",
              "displayName": "LeadsFound",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsWithEmails",
              "displayName": "LeadsWithEmails",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsWithoutEmails",
              "displayName": "LeadsWithoutEmails",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "DocumentID",
              "displayName": "DocumentID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsSheetID",
              "displayName": "LeadsSheetID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "RunMode",
              "displayName": "RunMode",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2384,
        16
      ],
      "id": "d128e612-2c41-4087-8525-8da5199963ed",
      "name": "Save Run History (New Sheet)",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const form = $('On form submission').first().json;\nconst aggregated = $('Aggregate').first().json.data || [];\n\nfunction hasEmail(raw) {\n  if (raw === undefined || raw === null) return false;\n  const value = String(raw).trim().toLowerCase();\n  return value !== '' && value !== 'not found';\n}\n\nconst leadsFound = aggregated.length;\nconst leadsWithEmails = aggregated.filter((item) => hasEmail(item.email)).length;\n\nreturn [{\n  json: {\n    RunAtUTC: new Date().toISOString(),\n    SearchQuery: form['Search Locations Query'] ?? '',\n    PagesRequested: String(form['Pages per query to scrape'] ?? ''),\n    LeadsFound: String(leadsFound),\n    LeadsWithEmails: String(leadsWithEmails),\n    LeadsWithoutEmails: String(leadsFound - leadsWithEmails),\n    DocumentID: String(form['Google Sheets Document Id'] ?? ''),\n    LeadsSheetID: String(form['Google Sheets ID'] ?? ''),\n    RunMode: 'existing_sheet'\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1936,
        208
      ],
      "id": "b55e8dc4-eab9-4679-89ae-b7d01c3f99a3",
      "name": "Prepare Run Log (Existing Sheet)"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $('On form submission').item.json['Google Sheets Document Id'] }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Runs",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [
            {
              "id": "RunAtUTC",
              "displayName": "RunAtUTC",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "SearchQuery",
              "displayName": "SearchQuery",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "PagesRequested",
              "displayName": "PagesRequested",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsFound",
              "displayName": "LeadsFound",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsWithEmails",
              "displayName": "LeadsWithEmails",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsWithoutEmails",
              "displayName": "LeadsWithoutEmails",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "DocumentID",
              "displayName": "DocumentID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "LeadsSheetID",
              "displayName": "LeadsSheetID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "RunMode",
              "displayName": "RunMode",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2160,
        208
      ],
      "id": "c7c0b0f8-d86d-4c83-89fe-e61a81914009",
      "name": "Save Run History (Existing Sheet)",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Search Google Maps": {
      "main": [
        [
          {
            "node": "Aggregate1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Email from Website": {
      "main": [
        [
          {
            "node": "Email Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Found?": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google for Email": {
      "main": [
        [
          {
            "node": "Extract Email from Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Email from Google": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Create Spreadsheet": {
      "main": [
        [
          {
            "node": "Structure data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Is google sheet empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out1": {
      "main": [
        [
          {
            "node": "Search Google Maps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate1": {
      "main": [
        [
          {
            "node": "Remove Duplicates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicates": {
      "main": [
        [
          {
            "node": "Is there a website?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Queries": {
      "main": [
        [
          {
            "node": "Split Out1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Websites": {
      "main": [
        [
          {
            "node": "Extract Email from Website",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "node": "Prepare Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is there a website?": {
      "main": [
        [
          {
            "node": "Scrape Websites",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "Search Google for Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is google sheet empty": {
      "main": [
        [
          {
            "node": "Create Spreadsheet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Combine with old and remove duplicates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine with old and remove duplicates": {
      "main": [
        [
          {
            "node": "Save leads to existing sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structure data": {
      "main": [
        [
          {
            "node": "Save Leads to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Leads to Sheet": {
      "main": [
        [
          {
            "node": "Prepare Run Log (New Sheet)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Run Log (New Sheet)": {
      "main": [
        [
          {
            "node": "Save Run History (New Sheet)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save leads to existing sheet": {
      "main": [
        [
          {
            "node": "Prepare Run Log (Existing Sheet)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Run Log (Existing Sheet)": {
      "main": [
        [
          {
            "node": "Save Run History (Existing Sheet)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "e529f524-83f6-4784-a64c-ce4cb3137618",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "8yOJhMP3aH6rztyb",
  "tags": []
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

Google Maps Lead Generation with Email Discovery. Uses @hasdata/n8n-nodes-hasdata, googleSheets, formTrigger. Event-driven trigger; 25 nodes.

Source: https://gist.github.com/CursedCode45/1c2408cee3649d12d8c0c3b9e513f57e — 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

The Recap AI - Insurance Lawyer Lead Gen. Uses executeWorkflowTrigger, formTrigger, @mendable/n8n-nodes-firecrawl, googleSheets. Event-driven trigger; 33 nodes.

Execute Workflow Trigger, Form Trigger, @Mendable/N8N Nodes Firecrawl +4
Marketing & Ads

This n8n workflow monitors Reddit for posts relevant to a specific business or industry, identifies potential leads, and delivers them directly to your inbox.

OpenRouter Chat, HTTP Request Tool, Output Parser Structured +5
Marketing & Ads

Trending YouTube Videos copy. Uses googleSheets, openAi, httpRequest, stickyNote. Event-driven trigger; 12 nodes.

Google Sheets, OpenAI, HTTP Request +1
Marketing & Ads

1. LI Connection Request System: Trigger PhantomBuster Agent. Uses formTrigger, httpRequest, openAi, limit. Event-driven trigger; 8 nodes.

Form Trigger, HTTP Request, OpenAI +1
Marketing & Ads

This workflow is designed to take user inputs in order to generate an image using the Riverflow 2.0 model through the Replicate API. It can handle both image generation as well as image editing. Addit

Form Trigger, Data Table, HTTP Request +1