This workflow corresponds to n8n.io template #15653 — we link there as the canonical source.
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 →
{
"id": "Yzo1A1eSuOsR56Up",
"name": "Lead Generation Flow",
"tags": [],
"nodes": [
{
"id": "52489c97-c115-42da-90ca-bca6eb33b6e5",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
160
],
"parameters": {
"width": 480,
"height": 896,
"content": "## Lead Generation Flow\n\n### How it works\n\n1. The workflow is triggered manually to start the lead generation process.\n2. A Google Sheet is created and read to retrieve configuration data.\n3. Search tasks are built from the configuration and executed on Google Maps.\n4. Search results are processed to remove duplicates and check for the presence of websites.\n5. Leads with or without emails are formatted and merged as needed.\n6. The final compiled leads are saved into a Google Sheets document.\n\n### Setup steps\n\n- [ ] Ensure Google Sheets API credentials are set up and configured.\n- [ ] Verify manual trigger settings are properly defined.\n- [ ] Ensure all nodes are authorized and have correct permissions.\n\n### Customization\n\nNodes related to data processing and lead formatting can be customized as per specific business needs."
},
"typeVersion": 1
},
{
"id": "195a09d3-88ca-411e-a869-049f68b4082b",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1264,
400
],
"parameters": {
"color": 7,
"width": 400,
"height": 304,
"content": "## Initialize workflow\n\nStart the workflow and create a results sheet in Google Sheets."
},
"typeVersion": 1
},
{
"id": "655603e9-ff25-4146-b038-b77243d46abf",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1712,
400
],
"parameters": {
"color": 7,
"width": 400,
"height": 304,
"content": "## Configure and prepare tasks\n\nRead configuration from the sheet and prepare search tasks."
},
"typeVersion": 1
},
{
"id": "954f39a5-84ac-425d-9f6e-0fd0f289df47",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2144,
400
],
"parameters": {
"color": 7,
"width": 608,
"height": 304,
"content": "## Execute and process searches\n\nPerform search on Google Maps, process results and remove duplicates."
},
"typeVersion": 1
},
{
"id": "2d8a914c-cff1-4754-87ef-60b2abfe637e",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2784,
160
],
"parameters": {
"color": 7,
"width": 816,
"height": 528,
"content": "## Website check and lead formatting\n\nCheck for website, scrape if available, and format leads with emails."
},
"typeVersion": 1
},
{
"id": "5199afef-907a-4403-8873-ed6e3ac16ac7",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2976,
720
],
"parameters": {
"color": 7,
"width": 576,
"height": 512,
"content": "## Google search fallback\n\nMerge leads for Google search and extract emails if needed."
},
"typeVersion": 1
},
{
"id": "3297ae67-92f7-486b-bbf3-db561cab26b7",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
3744,
352
],
"parameters": {
"color": 7,
"width": 368,
"height": 304,
"content": "## Merge and save leads\n\nMerge all accumulated leads and save to the results sheet in Google Sheets."
},
"typeVersion": 1
},
{
"id": "e342364a-db64-4af2-9a43-611191a71d04",
"name": "Start Lead Generation",
"type": "n8n-nodes-base.manualTrigger",
"notes": "Click to start the workflow manually",
"position": [
1312,
528
],
"parameters": {},
"typeVersion": 1
},
{
"id": "71b0af29-4728-4e8d-8fa7-f106e6a2817b",
"name": "Create Leads Sheet",
"type": "n8n-nodes-base.googleSheets",
"notes": "Creates a new sheet with timestamp to store results",
"position": [
1520,
528
],
"parameters": {
"title": "={{ $now.toFormat(\"yyyy-MM-dd HH:mm\") }}",
"options": {},
"operation": "create",
"documentId": {
"__rl": true,
"mode": "list",
"value": "1nfFNvbdD1dyUNWxRzuWiTzAODSxH8j5B06z_YKvOFg4",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nfFNvbdD1dyUNWxRzuWiTzAODSxH8j5B06z_YKvOFg4/edit?usp=drivesdk",
"cachedResultName": "Leads"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "93b417bb-d07a-4a65-a720-3562c9cc2c1a",
"name": "Read Configurations",
"type": "n8n-nodes-base.googleSheets",
"notes": "Reads search queries and parameters from config sheet",
"position": [
1760,
528
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "config"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.spreadsheetId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "858c782e-9445-4d88-b520-5e735d2697ca",
"name": "Initialize Search Tasks",
"type": "n8n-nodes-base.code",
"notes": "Converts config into multiple search tasks with pagination (20 results per page)",
"position": [
1968,
528
],
"parameters": {
"jsCode": "// 1. Initialize an array to store all tasks\nlet allTasks = [];\n\n// 2. Constants\nconst RESULTS_PER_PAGE = 20;\n\n// 3. Loop through Google Sheets rows\nfor (const item of $input.all()) {\n const config = item.json;\n \n // Validation: basic check for the query\n if (!config.leads_query) continue;\n\n const leads_query = config.leads_query;\n const location = config.location || \"\"; \n const pageCount = parseInt(config.page_count) || 1; \n \n // Get the starting page number from the table (default to 1 if empty)\n const startPage = parseInt(config.offset) || 1;\n\n // Generate a task for each requested page\n for (let p = 0; p < pageCount; p++) {\n const currentPageNumber = startPage + p;\n \n // Convert Page Number to API Position (Offset)\n // Page 1 -> (1-1) * 20 = 0\n // Page 2 -> (2-1) * 20 = 20\n const apiPosition = (currentPageNumber - 1) * RESULTS_PER_PAGE;\n \n allTasks.push({\n json: {\n leads_query: leads_query,\n ll: \"\",\n location: location,\n pageOffset: apiPosition, // This goes to the API\n pageNumber: currentPageNumber, // Optional: for your own tracking\n original_row: config.row_number || null \n }\n });\n }\n}\n\n// 4. Return the tasks\nreturn allTasks;"
},
"typeVersion": 2
},
{
"id": "41c10285-b329-4dba-afb3-bf8e230270c3",
"name": "Search in Google Maps",
"type": "@hasdata/n8n-nodes-hasdata.hasData",
"notes": "Fetches business listings from Google Maps based on query and location",
"onError": "continueRegularOutput",
"position": [
2192,
528
],
"parameters": {
"q": "={{ $json.leads_query }} {{ $json.location }}",
"resource": "google_maps",
"additionalFields": {
"ll": "=@40.7455096,-74.0083012,14z",
"start": "={{ $json.pageOffset }}"
}
},
"credentials": {
"hasDataApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "8bff1721-88b0-4a60-b6a0-7083061aa6bd",
"name": "Split Local Results",
"type": "n8n-nodes-base.splitOut",
"notes": "Separates each business into individual items for processing",
"position": [
2400,
528
],
"parameters": {
"include": "=",
"options": {},
"fieldToSplitOut": "localResults"
},
"typeVersion": 1
},
{
"id": "ee185f1e-3bd3-4b73-9579-108076c10727",
"name": "Deduplicate Local Results",
"type": "n8n-nodes-base.removeDuplicates",
"notes": "Removes duplicate businesses based on name and address",
"position": [
2608,
528
],
"parameters": {
"compare": "selectedFields",
"options": {},
"fieldsToCompare": "localResults.title, localResults.kgmid"
},
"typeVersion": 2
},
{
"id": "fbe0b3e9-7bfb-4049-a56d-2e45217a2b8e",
"name": "Check Website Availability",
"type": "n8n-nodes-base.if",
"notes": "Checks if business has a website - TRUE path scrapes it, FALSE path searches Google",
"position": [
2832,
528
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d2c86a6c-2c9d-472e-a328-71d92bdc74f2",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.localResults.website }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "43df8509-5edd-407d-a623-4a4ddf8130b4",
"name": "Scrape Website Data",
"type": "@hasdata/n8n-nodes-hasdata.hasData",
"notes": "Scrapes the business website to extract email addresses",
"onError": "continueRegularOutput",
"position": [
3040,
432
],
"parameters": {
"url": "={{ $json.localResults.website }}",
"resource": "web_scraping",
"additionalFields": {
"extractEmails": true
}
},
"credentials": {
"hasDataApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "13e49a8f-4514-40e3-a908-85c3db9bc25a",
"name": "Check for Emails",
"type": "n8n-nodes-base.if",
"notes": "Checks if emails were found on website - TRUE adds them, FALSE searches Google",
"position": [
3248,
368
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5590ef2a-7d53-4637-afa0-10acdeb7dc41",
"operator": {
"type": "array",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.emails }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "e0fc9eca-b3ab-4331-8a03-7a49089020bd",
"name": "Format Lead With Contact",
"type": "n8n-nodes-base.set",
"notes": "Prepares lead data including scraped emails for output",
"position": [
3456,
272
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a574f281-57df-4ee8-9d56-945f141b5345",
"name": "title",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.title }}"
},
{
"id": "f92a90cd-b7ea-4740-8d0e-739d18912fa4",
"name": "phone",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.phone }}"
},
{
"id": "c6a92d26-418d-41f0-86d1-4fe67755bf94",
"name": "address",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.address }}"
},
{
"id": "5a1e6e7c-559f-4419-9719-73de143ba76a",
"name": "website",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.website }}"
},
{
"id": "3ebc4a23-0bb6-43a0-b8f1-804acc29793b",
"name": "emails",
"type": "string",
"value": "={{ $json.emails.join(', ') }}"
},
{
"id": "03dd5a32-cc01-4cab-872b-8d18a40797cc",
"name": "rating",
"type": "number",
"value": "={{ $('Check Website Availability').item.json.localResults.rating }}"
},
{
"id": "361d3f7a-bfd4-40a7-901c-13e8e895d1fa",
"name": "reviews",
"type": "number",
"value": "={{ $('Check Website Availability').item.json.localResults.reviews }}"
},
{
"id": "b1a783ca-5559-4e2d-8d28-5b98140cc89d",
"name": "type",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.types.join(', ') }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a045bb75-6744-4973-8507-5921be1a48db",
"name": "Format Lead Without Contact",
"type": "n8n-nodes-base.set",
"notes": "Prepares lead data without emails (will search Google next)",
"position": [
3392,
832
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "ad7fae25-b7e0-46ab-ad95-568ab9c6813a",
"name": "localResults.title",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.title }}"
},
{
"id": "183dca66-d14a-4a63-9c14-30fffdaf4745",
"name": "localResults.phone",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.phone }}"
},
{
"id": "d2791f5a-9e15-4d29-b1a7-488137c0deb8",
"name": "localResults.address",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.address }}"
},
{
"id": "0d935b92-cb1e-4779-bd7e-3af1d2dd38ba",
"name": "localResults.website",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.website }}"
},
{
"id": "faff970a-cb9d-4511-abb3-c97908cfdf59",
"name": "rating",
"type": "number",
"value": "={{ $('Check Website Availability').item.json.localResults.rating }}"
},
{
"id": "f4252269-c3b3-4ee2-a45c-458d3021123d",
"name": "reviews",
"type": "number",
"value": "={{ $('Check Website Availability').item.json.localResults.reviews }}"
},
{
"id": "4cc0b0f7-e726-4c69-8682-c87a75306efb",
"name": "type",
"type": "string",
"value": "={{ $('Check Website Availability').item.json.localResults.types.join(', ') }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "cf5ffcbe-8bf1-4364-a500-a6342c1d0f11",
"name": "Merge Leads for Google Search",
"type": "n8n-nodes-base.merge",
"notes": "Combines leads without website or without scraped emails for Google search",
"position": [
3024,
1072
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "a91249fc-dc97-456a-9b00-e01b3cf302bd",
"name": "Find Emails Via Google",
"type": "@hasdata/n8n-nodes-hasdata.hasData",
"notes": "Searches Google to find email addresses for businesses",
"onError": "continueRegularOutput",
"position": [
3200,
1072
],
"parameters": {
"q": "={{ $json.localResults.title }}, {{ $json.localResults.address.split(', ').slice(1).join(', ') }} email \"@\"",
"additionalFields": {}
},
"credentials": {
"hasDataApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 1,
"waitBetweenTries": 5000
},
{
"id": "67f5888f-9ebd-4ac5-aa95-96acdfb1bbb9",
"name": "Extract Emails",
"type": "n8n-nodes-base.code",
"notes": "Extracts email addresses from Google search results using regex",
"position": [
3408,
1072
],
"parameters": {
"jsCode": "/**\n * Optimized lead processor for n8n.\n * Features: Ancestor matching for Merge1 data and advanced email cleanup.\n */\n\nlet finalResults = [];\n\n// Iterate through items coming from the previous node\nfor (let i = 0; i < $input.all().length; i++) {\n const item = $input.all()[i];\n const data = item.json;\n const results = data.organicResults || [];\n\n /**\n * Safe data retrieval from Merge for Google Search.\n * Using itemMatching(i) ensures we sync correctly with previous workflow steps.\n */\n let sourceData = {};\n let fullData = {};\n try {\n const ancestor = $('Merge Leads for Google Search').itemMatching(i).json;\n sourceData = ancestor.localResults || ancestor;\n fullData = ancestor;\n } catch (e) {\n const fallback = $('Merge Leads for Google Search').all()[i]?.json;\n sourceData = fallback?.localResults || fallback || {};\n fullData = fallback;\n }\n\n let foundData = {\n website: null,\n emails: null,\n title: sourceData.title || null,\n phone: sourceData.phone || null,\n address: sourceData.address || null,\n rating: fullData.rating || sourceData.rating || null,\n reviews: fullData.reviews || sourceData.reviews || null,\n type: fullData.type || sourceData.type || null\n };\n\n /**\n * REFINED EMAIL REGEX\n * [a-z]{2,6} - Look for lowercase extensions only.\n * (?![A-Z]) - Negative lookahead: stop if a Capital letter follows (like .Read).\n */\n const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-z]{2,6}(?![A-Z])/;\n\n for (const res of results) {\n const textToScan = (res.snippet || \"\") + \" \" + (res.title || \"\");\n const match = textToScan.match(emailRegex);\n\n if (match) {\n let cleanedEmail = match[0];\n \n /**\n * MANUAL ARTIFACT REMOVAL\n * Cleans up cases where snippet text is glued to the email.\n */\n const artifacts = [\".Read\", \".View\", \".More\", \".Check\", \".Full\", \".Website\"];\n for (const art of artifacts) {\n if (cleanedEmail.endsWith(art)) {\n cleanedEmail = cleanedEmail.slice(0, -art.length);\n }\n }\n\n // Final trim for any trailing dots\n cleanedEmail = cleanedEmail.replace(/\\.+$/, \"\"); \n\n foundData.emails = cleanedEmail;\n foundData.website = res.link;\n \n // Stop at the first valid email found in the results list\n break; \n }\n }\n\n finalResults.push({ json: foundData });\n}\n\nreturn finalResults;"
},
"typeVersion": 2
},
{
"id": "e3b87736-1a90-40bd-9726-5bf0fc78c525",
"name": "Combine All Leads",
"type": "n8n-nodes-base.merge",
"notes": "Combines all leads (with website emails and Google-found emails)",
"position": [
3792,
480
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "885b6f48-19cd-433d-82f6-5c6bb6d68989",
"name": "Append to Leads Sheet",
"type": "n8n-nodes-base.googleSheets",
"notes": "Writes all found leads to the timestamped results sheet",
"position": [
3968,
480
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "phone",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "address",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "website",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "website",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "emails",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "emails",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rating",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "rating",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "reviews",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "reviews",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "type",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "type",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Create Leads Sheet').first().json.title }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Create Leads Sheet').first().json.spreadsheetId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "9304af19-7674-4edf-8700-64c1e45bdb11",
"connections": {
"Extract Emails": {
"main": [
[
{
"node": "Combine All Leads",
"type": "main",
"index": 1
}
]
]
},
"Check for Emails": {
"main": [
[
{
"node": "Format Lead With Contact",
"type": "main",
"index": 0
}
],
[
{
"node": "Format Lead Without Contact",
"type": "main",
"index": 0
}
]
]
},
"Combine All Leads": {
"main": [
[
{
"node": "Append to Leads Sheet",
"type": "main",
"index": 0
}
]
]
},
"Create Leads Sheet": {
"main": [
[
{
"node": "Read Configurations",
"type": "main",
"index": 0
}
]
]
},
"Read Configurations": {
"main": [
[
{
"node": "Initialize Search Tasks",
"type": "main",
"index": 0
}
]
]
},
"Scrape Website Data": {
"main": [
[
{
"node": "Check for Emails",
"type": "main",
"index": 0
}
]
]
},
"Split Local Results": {
"main": [
[
{
"node": "Deduplicate Local Results",
"type": "main",
"index": 0
}
]
]
},
"Search in Google Maps": {
"main": [
[
{
"node": "Split Local Results",
"type": "main",
"index": 0
}
]
]
},
"Start Lead Generation": {
"main": [
[
{
"node": "Create Leads Sheet",
"type": "main",
"index": 0
}
]
]
},
"Find Emails Via Google": {
"main": [
[
{
"node": "Extract Emails",
"type": "main",
"index": 0
}
]
]
},
"Initialize Search Tasks": {
"main": [
[
{
"node": "Search in Google Maps",
"type": "main",
"index": 0
}
]
]
},
"Format Lead With Contact": {
"main": [
[
{
"node": "Combine All Leads",
"type": "main",
"index": 0
}
]
]
},
"Deduplicate Local Results": {
"main": [
[
{
"node": "Check Website Availability",
"type": "main",
"index": 0
}
]
]
},
"Check Website Availability": {
"main": [
[
{
"node": "Scrape Website Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Merge Leads for Google Search",
"type": "main",
"index": 1
}
]
]
},
"Format Lead Without Contact": {
"main": [
[
{
"node": "Merge Leads for Google Search",
"type": "main",
"index": 0
}
]
]
},
"Merge Leads for Google Search": {
"main": [
[
{
"node": "Find Emails Via Google",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
googleSheetsOAuth2ApihasDataApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This n8n template shows how to build a complete B2B lead generation pipeline that scrapes Google Maps for businesses and enriches them with emails from their websites or Google search.
Source: https://n8n.io/workflows/15653/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
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
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
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
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