This workflow corresponds to n8n.io template #17392 — 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 →
{
"id": "f9AthtT0W1WRa0iy",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Template 2- Google Maps Lead Scraper",
"tags": [],
"nodes": [
{
"id": "44c1fd25-8db4-45fe-9a28-d2082319ee8c",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"position": [
1952,
352
],
"parameters": {},
"typeVersion": 1
},
{
"id": "55876714-757c-42d4-b240-e592107b0c19",
"name": "Set Keyword & City",
"type": "n8n-nodes-base.set",
"position": [
2176,
352
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "asg-keyword",
"name": "keyword",
"type": "string",
"value": "plumber"
},
{
"id": "asg-city",
"name": "city",
"type": "string",
"value": "Manchester"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "65bb44dc-75fa-4d16-b43e-760599c3ad10",
"name": "ScrapeUnblocker",
"type": "n8n-nodes-scrapeunblocker.scrapeUnblocker",
"position": [
2400,
352
],
"parameters": {
"url": "={{ 'https://www.google.com/maps/search/' + encodeURIComponent(($json.keyword + ' ' + $json.city).trim()) }}",
"proxy_country": "US"
},
"credentials": {},
"typeVersion": 1
},
{
"id": "41a6412d-c207-4f15-a435-a1d4f4c7ad99",
"name": "Extract Map Listings",
"type": "n8n-nodes-base.code",
"position": [
2608,
352
],
"parameters": {
"jsCode": "// ===== SHARED BOILERPLATE (same in every template) =====\nlet html = \"\";\nconst first = $input.first();\nif (first) {\n const j = first.json;\n if (typeof j === 'string') {\n html = j;\n } else if (j && typeof j === 'object') {\n html = j.data || j.html || j.body || JSON.stringify(j);\n }\n}\nif (!html || html.trim() === \"\" || html === \"{}\") {\n return [{ json: { error: \"No HTML received. Check the ScrapeUnblocker node output to ensure it is actually loading the page.\" } }];\n}\n// ===== END BOILERPLATE =====\n\n// Search context (set before scraping)\nlet keyword = \"\", city = \"\";\ntry {\n const ctx = $('Set Keyword & City').first().json;\n keyword = ctx.keyword || \"\";\n city = ctx.city || \"\";\n} catch (e) {}\nconst today = new Date().toISOString().split('T')[0];\n\n// helpers\nconst grab = (s, re) => { const m = s.match(re); return m ? m[1].trim() : \"\"; };\nconst decode = (t) => t\n .replace(/&/g, '&').replace(/'/g, \"'\").replace(/"/g, '\"')\n .replace(/</g, '<').replace(/>/g, '>').replace(/ /g, ' ').trim();\n\n// One chunk per result card\nconst chunks = html.split(/class=\"hfpxzc\"/i);\n\nconst leads = [];\nfor (let i = 1; i < chunks.length; i++) {\n const c = chunks[i];\n\n // NAME: read from the headline div INSIDE the card.\n // (The anchor's aria-label sits BEFORE the split point, so it belongs to the previous card \u2014 do not use it.)\n const name = decode(grab(c, /class=\"qBF1Pd[^\"]*\">\\s*([^<]+?)\\s*</i));\n if (!name) continue;\n\n const rating = grab(c, /class=\"MW4etd\">\\s*([\\d.]+)/i);\n const reviews = grab(c, /aria-label=\"[\\d.]+ stars\\s*([\\d,]+)\\s*[Rr]eview/i).replace(/,/g, '');\n const category = decode(grab(c, /class=\"W4Efsd\">\\s*<span>\\s*<span>\\s*([^<]+?)\\s*</i));\n const phone = grab(c, /class=\"UsdlK\">\\s*([^<]+?)\\s*</i);\n const website = grab(c, /aria-label=\"Visit[^\"]*website\"[\\s\\S]*?href=\"([^\"]+)\"/i);\n\n // LAT / LNG: embedded in the place URL as !3dLAT!4dLNG\n const coord = c.match(/!3d(-?\\d+\\.\\d+)!4d(-?\\d+\\.\\d+)/);\n const lat = coord ? coord[1] : \"\";\n const lng = coord ? coord[2] : \"\";\n\n // ADDRESS: often absent for service businesses (Maps shows category + hours instead).\n // Best-effort: pick a W4Efsd text block that actually looks like an address (has a digit AND a comma).\n let address = \"\";\n const blocks = c.match(/class=\"W4Efsd\">\\s*<span>\\s*<span>\\s*([^<]+?)\\s*</gi) || [];\n for (const b of blocks) {\n const txt = decode(b.replace(/class=\"W4Efsd\">\\s*<span>\\s*<span>\\s*/i, \"\").replace(/\\s*<$/, \"\"));\n if (/\\d/.test(txt) && /,/.test(txt)) { address = txt; break; }\n }\n\n leads.push({\n json: {\n Name: name,\n Rating: rating,\n Reviews: reviews,\n Category: category,\n Address: address,\n Latitude: lat,\n Longitude: lng,\n Phone: phone,\n Website: website,\n Keyword: keyword,\n City: city,\n Date: today\n }\n });\n}\n\nif (leads.length === 0) {\n return [{ json: { error: \"HTML received but 0 listings parsed. Class names may have changed \u2014 inspect the ScrapeUnblocker output and adjust selectors.\", scraped_chars: html.length } }];\n}\n\nreturn leads;"
},
"typeVersion": 2
},
{
"id": "8359aa0a-4378-4745-853f-1da51b6a0593",
"name": "Clean & Dedupe Leads",
"type": "n8n-nodes-base.code",
"position": [
2816,
352
],
"parameters": {
"jsCode": "const seen = new Set();\nconst out = [];\n\nfor (const item of $input.all()) {\n const j = item.json;\n if (j.error) { out.push({ json: j }); continue; }\n\n const key = (j.Name || \"\").toLowerCase().trim();\n if (!key || seen.has(key)) continue;\n seen.add(key);\n\n const phone = (j.Phone || \"\").replace(/[^\\d+]/g, \"\");\n\n out.push({\n json: {\n Name: j.Name || \"\",\n Rating: j.Rating || \"\",\n Reviews: j.Reviews || \"\",\n Category: j.Category || \"\",\n Address: j.Address || \"\",\n Latitude: j.Latitude || \"\",\n Longitude: j.Longitude || \"\",\n Phone: phone,\n Website: j.Website || \"\",\n Keyword: j.Keyword || \"\",\n City: j.City || \"\",\n Date: j.Date || \"\"\n }\n });\n}\n\nreturn out;"
},
"typeVersion": 2
},
{
"id": "58089616-59f6-4200-9be0-15e83152637f",
"name": "Save Leads to Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
3680,
352
],
"parameters": {
"columns": {
"value": {
"City": "={{ $json.City }}",
"Date": "={{ $json.Date }}",
"Name": "={{ $json.Name }}",
"Phone": "={{ $json.Phone }}",
"Rating": "={{ $json.Rating }}",
"Address": "={{ $json.Address }}",
"GeoCity": "={{ $json.GeoCity }}",
"Keyword": "={{ $json.Keyword }}",
"Website": "={{ $json.Website }}",
"Category": "={{ $json.Category }}",
"Latitude": "={{ $json.Latitude }}",
"Postcode": "={{ $json.Postcode }}",
"Longitude": "={{ $json.Longitude }}",
"FullAddress": "={{ $json.FullAddress }}",
"GeoConfidence": "={{ $json.GeoConfidence }}"
},
"schema": [
{
"id": "Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Rating",
"type": "string",
"display": true,
"required": false,
"displayName": "Rating",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Category",
"type": "string",
"display": true,
"required": false,
"displayName": "Category",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Address",
"type": "string",
"display": true,
"required": false,
"displayName": "Address",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "FullAddress",
"type": "string",
"display": true,
"required": false,
"displayName": "FullAddress",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Postcode",
"type": "string",
"display": true,
"required": false,
"displayName": "Postcode",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "GeoCity",
"type": "string",
"display": true,
"required": false,
"displayName": "GeoCity",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Latitude",
"type": "string",
"display": true,
"required": false,
"displayName": "Latitude",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Longitude",
"type": "string",
"display": true,
"required": false,
"displayName": "Longitude",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Phone",
"type": "string",
"display": true,
"required": false,
"displayName": "Phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Website",
"type": "string",
"display": true,
"required": false,
"displayName": "Website",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "City",
"type": "string",
"display": true,
"required": false,
"displayName": "City",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "GeoConfidence",
"type": "string",
"display": true,
"required": false,
"displayName": "GeoConfidence",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_C0G2_iLcdadsmygjk3hqEyD4Rn7OzxnxapP7s-zXM/edit#gid=0",
"cachedResultName": "Leads"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1U_C0G2_iLcdadsmygjk3hqEyD4Rn7OzxnxapP7s-zXM",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U_C0G2_iLcdadsmygjk3hqEyD4Rn7OzxnxapP7s-zXM/edit?usp=drivesdk",
"cachedResultName": "Google Maps Lead Scraper"
}
},
"typeVersion": 4.5
},
{
"id": "6ed04089-70ba-4623-b32d-afa5ceccad94",
"name": "Get Adresses",
"type": "n8n-nodes-base.httpRequest",
"position": [
3264,
352
],
"parameters": {
"url": "https://nominatim.openstreetmap.org/reverse",
"options": {},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "format",
"value": "jsonv2"
},
{
"name": "lat",
"value": "={{ $json.Latitude }}"
},
{
"name": "lon",
"value": "={{ $json.Longitude }}"
},
{
"name": "addressdetails",
"value": "1"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "ProfitableMedia-LeadTool/1.0 (user@example.com@email.com)"
}
]
}
},
"typeVersion": 4.4
},
{
"id": "0d98f492-0b1d-4244-9d03-1061010e8b26",
"name": "Parse the output",
"type": "n8n-nodes-base.code",
"position": [
3472,
352
],
"parameters": {
"jsCode": "// HTTP node (Reverse Geocode) item pairing ki wajah se ye current lead deta hai\nconst lead = $('Clean & Dedupe Leads').item.json;\n\n// Nominatim ka response is node ke input me hai\nconst geo = $json || {};\nconst a = geo.address || {};\n\n// Full address (Nominatim ka display_name)\nconst fullAddress = geo.display_name || \"\";\n\n// Clean/short address (country/state hata ke)\nconst parts = [\n a.house_number,\n a.road,\n (a.suburb || a.town || a.village || a.quarter),\n a.city,\n a.postcode\n].filter(Boolean);\nconst cleanAddress = parts.join(\", \");\n\nconst postcode = a.postcode || \"\";\nconst city = a.city || a.town || a.village || \"\";\n\n// Quality flag: postcode na mile to pin shaky hai (jaise galat Google pin)\nconst geoConfidence = postcode ? \"yes\" : \"check\";\n\nreturn [{\n json: {\n // --- saari previous fields waise ki waise ---\n ...lead,\n\n // --- address fields add ---\n Address: cleanAddress || fullAddress,\n FullAddress: fullAddress,\n Postcode: postcode,\n GeoCity: city,\n GeoConfidence: geoConfidence\n }\n}];"
},
"typeVersion": 2
},
{
"id": "37daf016-28d8-4480-8d5c-ef89c3280876",
"name": "Loop over Leads",
"type": "n8n-nodes-base.splitInBatches",
"position": [
3056,
352
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "f4004d5c-6c84-4c09-ae91-f915f481a602",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1216,
96
],
"parameters": {
"width": 592,
"height": 688,
"content": "## Google Maps Lead Scraper & Reverse Geocoder\n\n### How It Works\n* **Triggers & Search:** Takes a target `keyword` and `city`, then fetches Google Maps HTML via **ScrapeUnblocker**.\n* **Extraction:** Parses card HTML using regex to extract business name, rating, reviews, category, phone, website, and `Lat`/`Lng` coordinates.\n* **Cleaning:** Sanitizes phone numbers and removes duplicate business entries by name.\n* **Enrichment:** Loops each lead through **Nominatim (OpenStreetMap)** to convert coordinates into structured full addresses, postcodes, and a `GeoConfidence` score.\n* **Storage:** Appends all enriched lead data directly to your designated **Google Sheet**.\n\n### Quick Setup Checklist\n1. **ScrapeUnblocker:** Connect active credentials.\n2. **Google Sheets:** Authenticate account and verify spreadsheet ID, sheet name (`Leads`), and column mappings.\n3. **HTTP Request:** Ensure the `User-Agent` header in **Get Adresses** remains set for OpenStreetMap compliance."
},
"typeVersion": 1
},
{
"id": "8f21eeeb-011a-4f96-8916-9488b2de03cf",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1888,
160
],
"parameters": {
"color": 7,
"width": 448,
"height": 368,
"content": "## 1. Input Parameters & Map Scraping\nDefines search criteria (keyword & city), constructs the encoded Google Maps URL, and fetches page HTML via ScrapeUnblocker."
},
"typeVersion": 1
},
{
"id": "9fce125f-1c58-4733-97c7-04d2e9aab5b7",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2368,
160
],
"parameters": {
"color": 7,
"width": 592,
"height": 368,
"content": "## 2. Parsing, Cleaning & Deduplication\n"
},
"typeVersion": 1
},
{
"id": "868407db-9b50-4e02-87d0-bd768292836b",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2992,
160
],
"parameters": {
"color": 7,
"width": 880,
"height": 384,
"content": "## 3. Reverse Geocoding Loop & Google Sheets Export\nLoops through extracted leads, calls Nominatim to convert Lat/Lng into structured postal addresses, and appends enriched records to Google Sheets."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "092615a1-9257-46e3-a723-04e0e60b17a0",
"nodeGroups": [],
"connections": {
"Get Adresses": {
"main": [
[
{
"node": "Parse the output",
"type": "main",
"index": 0
}
]
]
},
"Loop over Leads": {
"main": [
[],
[
{
"node": "Get Adresses",
"type": "main",
"index": 0
}
]
]
},
"ScrapeUnblocker": {
"main": [
[
{
"node": "Extract Map Listings",
"type": "main",
"index": 0
}
]
]
},
"Parse the output": {
"main": [
[
{
"node": "Save Leads to Sheet",
"type": "main",
"index": 0
}
]
]
},
"Set Keyword & City": {
"main": [
[
{
"node": "ScrapeUnblocker",
"type": "main",
"index": 0
}
]
]
},
"Save Leads to Sheet": {
"main": [
[
{
"node": "Loop over Leads",
"type": "main",
"index": 0
}
]
]
},
"Clean & Dedupe Leads": {
"main": [
[
{
"node": "Loop over Leads",
"type": "main",
"index": 0
}
]
]
},
"Extract Map Listings": {
"main": [
[
{
"node": "Clean & Dedupe Leads",
"type": "main",
"index": 0
}
]
]
},
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Set Keyword & City",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow scrapes Google Maps search results for a chosen keyword and city using ScrapeUnblocker, extracts and deduplicates business leads, reverse-geocodes each listing’s coordinates with OpenStreetMap Nominatim, and appends the enriched lead data to Google Sheets. Runs…
Source: https://n8n.io/workflows/17392/ — 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 workflow reads LinkedIn profile URLs from Google Sheets, enriches each lead using PhantomBuster profile and company scrapers, finds a work email via Hunter.io with a Dropcontact fallback, writes