This workflow corresponds to n8n.io template #10657 — 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": "OOBsbQF1kAFsaXgs",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "LEAD GEN",
"tags": [],
"nodes": [
{
"id": "7fd7c5a4-fb26-4f15-a87c-1c20da9cee77",
"name": "When clicking \u2018Test workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
0,
0
],
"parameters": {},
"typeVersion": 1
},
{
"id": "61a49dcf-c81d-40bd-8ddc-35b6d1c1719d",
"name": "SERPAPI- GOOGE SEARCH",
"type": "n8n-nodes-base.httpRequest",
"position": [
220,
0
],
"parameters": {
"url": "https://serpapi.com/search",
"options": {
"timeout": 30000,
"response": {
"response": {}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "engine",
"value": "google"
},
{
"name": "q",
"value": "plumbers in New York"
},
{
"name": "api_key",
"value": "YOUR-API-KEY"
},
{
"name": "num",
"value": "20"
},
{
"name": "type",
"value": "search"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "72440529-a439-4e7d-8a14-1def14609858",
"name": "Split Out",
"type": "n8n-nodes-base.splitOut",
"position": [
440,
0
],
"parameters": {
"options": {},
"fieldToSplitOut": "organic_results"
},
"typeVersion": 1
},
{
"id": "915c2553-aea1-4c52-a07e-8e637729342a",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"maxTries": 2,
"position": [
660,
0
],
"parameters": {
"url": "={{ $json.link }}",
"options": {
"redirect": {
"redirect": {}
},
"allowUnauthorizedCerts": false
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/0.0.0.0 Safari/537.36"
},
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.9"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": " https://www.google.com/"
},
{
"name": "DNT",
"value": "2"
},
{
"name": "Sec-Fetch-Mode",
"value": "navigate"
},
{
"name": "Sec-Fetch-Site",
"value": "cross-site"
},
{
"name": "Connection",
"value": "keep-alive"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"waitBetweenTries": 3000
},
{
"id": "619a41f3-a5a3-43d6-9b8c-9093033a3482",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
920,
0
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3.1
},
{
"id": "35c55e45-a4dd-412a-90f1-b0fb998d4251",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
1140,
0
],
"parameters": {
"jsCode": "const items = [];\nconst doScrapeFallback = true; // set false to skip scraping fallback\n\nconst today = () => new Date().toISOString().split('T')[0];\n\nfunction normalizeSiteUrl(u){\n if(!u || typeof u !== 'string') return '';\n u = u.trim();\n // strip trackers/params\n u = u.replace(/\\?.*$/,'').replace(/\\/$/,'');\n if(!/^https?:\\/\\//i && /^[\\w.-]+\\.[a-z]{2,}$/i.test(u)) u = 'https://' + u;\n return u;\n}\n\nfunction normalizeEmail(e){\n if(!e) return '';\n e = e.toLowerCase().trim();\n // reject placeholders\n const bad = ['your@email.com','user@example.com','user@example.com','user@example.com','user@example.com','user@example.com'];\n if(bad.includes(e)) return '';\n if(!/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/.test(e)) return '';\n return e;\n}\n\nfunction normalizePhone(p){\n if(!p) return '';\n // keep + then digits\n const plus = p.trim().startsWith('+') ? '+' : '';\n const digits = p.replace(/[^\\d]/g,'');\n if(digits.length < 7) return '';\n if(/^0+$/.test(digits)) return '';\n if(/^(\\d)\\1{5,}$/.test(digits)) return '';\n return plus + digits;\n}\n\nfunction findEmailsInText(text){\n if(!text) return [];\n const r = text.match(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g) || [];\n return Array.from(new Set(r.map(e => normalizeEmail(e)).filter(Boolean)));\n}\n\nfunction findPhonesInText(text){\n if(!text) return [];\n const r = text.match(/(?:\\+?\\d{1,3}[-.\\s]?)?(?:\\(?\\d{2,4}\\)?[-.\\s]?)?(?:\\d{2,4}[-.\\s]?){1,4}\\d{2,4}/g) || [];\n const norm = Array.from(new Set(r.map(p => normalizePhone(p)).filter(Boolean)));\n return norm;\n}\n\nfunction stripHtml(html){\n if(!html) return '';\n return html.replace(/<script[\\s\\S]*?>[\\s\\S]*?<\\/script>/gi,' ')\n .replace(/<style[\\s\\S]*?>[\\s\\S]*?<\\/style>/gi,' ')\n .replace(/<\\/?[^>]+(>|$)/g,' ')\n .replace(/\\s{2,}/g,' ').trim();\n}\n\n// Heuristic: prefer email matching website domain\nfunction scoreEmailAgainstSite(email, site){\n if(!email || !site) return 0;\n try {\n const ed = email.split('@').pop();\n const sd = (new URL(normalizeSiteUrl(site))).hostname.replace(/^www\\./i,'');\n if(ed && sd && ed.includes(sd)) return 5;\n } catch(e){}\n return 1;\n}\n\nfor(const it of $input.all()){\n const j = it.json || {};\n\n // Detect if the item is from serpapi local_results place\n const isLocalPlace = !!(j.phone || (j.links && j.links.website) || j.title);\n\n // Candidate fields (prefer structured SerpAPI fields)\n let business_name = (j.title || j.name || j.business_name || j.place_name || j.company) || '';\n let website = (j.links && (j.links.website || j.links.website)) || j.website || j.displayed_link || j.link || '';\n let snippet = (j.snippet || j.description || j.snippet_highlighted_words?.join(' ') || j.rich_snippet?.top?.extensions?.join(' ') || '') || '';\n let email = '';\n let phone = '';\n\n // If SerpAPI local_results has explicit phone or website\n if(j.phone) phone = normalizePhone(j.phone);\n if(j.links && j.links.website) website = j.links.website;\n\n // If structured contact fields exist\n if(j.email) email = normalizeEmail(j.email);\n if(j.contact_email) email = normalizeEmail(j.contact_email);\n\n // If the item includes a 'snippet' or 'description', try to extract emails/phones from it\n const candidateText = [\n JSON.stringify(j),\n j.snippet || '',\n j.description || '',\n j.rich_snippet && JSON.stringify(j.rich_snippet) || ''\n ].join(' ');\n\n if(!email){\n const emailsFound = findEmailsInText(candidateText);\n if(emailsFound.length) {\n // prefer email matching website domain\n emailsFound.sort((a,b) => scoreEmailAgainstSite(b,website) - scoreEmailAgainstSite(a,website));\n email = emailsFound[0] || '';\n }\n }\n\n if(!phone){\n const phonesFound = findPhonesInText(candidateText);\n phone = phonesFound[0] || '';\n }\n\n // If still missing and Scrape fallback allowed, try to parse item.json.data (if Scrape Website ran before)\n // Note: when this node runs BEFORE Scrape Website, item.json.data likely not present.\n if(doScrapeFallback && (!email || !phone) && j.data && typeof j.data === 'string'){\n const html = j.data;\n if(!email){\n const e2 = findEmailsInText(html);\n if(e2.length) email = e2[0];\n }\n if(!phone){\n const p2 = findPhonesInText(html);\n if(p2.length) phone = p2[0];\n }\n }\n\n // Final normalization\n website = normalizeSiteUrl(website);\n business_name = (business_name && business_name.toString().trim()) || '';\n snippet = stripHtml(snippet).slice(0,1000);\n\n // If both business_name and website empty, try to derive name from displayed_link or domain\n if(!business_name && website){\n try {\n business_name = (new URL(website)).hostname.replace(/^www\\./,'');\n } catch(e){}\n }\n\n // Decide if useful (avoid placeholder-only results)\n const useful = business_name.length > 1 || website.length > 5 || email.length > 4 || phone.replace(/[^\\d]/g,'').length >= 7 || snippet.length > 20;\n if(!useful) continue;\n\n items.push({\n json: {\n business_name: business_name || 'N/A',\n website: website || 'N/A',\n snippet: snippet || 'N/A',\n email: email || 'Not found',\n phone: phone || 'Not found',\n position: j.position || j.rank || 'N/A',\n scraped_date: today()\n }\n });\n}\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "de54e4b6-7ca7-4897-b769-2d18a34e9f09",
"name": "Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
1360,
0
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "business_name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "business_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "website",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "website",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "snippet",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "snippet",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "email",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "email",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "phone",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "position",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "position",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "scraped_date",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "scraped_date",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"cachedResultName": "LEADS"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "117bMXKXElY_5ican1lkpZ-y_-hDRHz3_Bjr-oN2IhyU",
"cachedResultUrl": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"cachedResultName": "LEAD AUTOMATION"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "5b453487-8d98-4572-9bdf-ad6cc3917109",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-60,
-120
],
"parameters": {
"color": 4,
"width": 1640,
"height": 380,
"content": "# LEAD GENERATION USING SERP-API\n"
},
"typeVersion": 1
},
{
"id": "ca9d18fc-9abd-434e-8582-0bc87ac81eb3",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-600,
-380
],
"parameters": {
"width": 520,
"height": 1120,
"content": "# \ud83e\uddf2 Lead Generation Automation (n8n + SerpAPI)\n\n# \ud83c\udfa5 **Watch the Tutorial**\n[](https://youtu.be/gr9_wEMc9sM?si=YB-TEjdGgu5oC_Z5)\n\nGenerate verified business leads automatically from Google Search using **SerpAPI** and **n8n**. \nThis workflow scrapes search results, extracts **emails, websites, phone numbers, and company names**, and saves everything directly into **Google Sheets**.\n\n### \u2699\ufe0f How It Works\n1. Searches Google using SerpAPI \n2. Extracts contact details via regex and validation \n3. Stores structured leads in Google Sheets \n\n### \ud83d\udcbc Use Cases\n- Local business prospecting \n- Email outreach campaigns \n- CRM data enrichment \n\nQuick setup in under 10 minutes \u2014 just add your **SerpAPI key** and **Google Sheets connection**.\n\n---\n\n\ud83d\udd17 **Created by:** Muhammad Shaheer \n\ud83d\udca1 **YouTube Channel:** [https://www.youtube.com/@BOTNEXA](https://www.youtube.com/@ShaheerAutomation) \n\ud83d\udce7 **For collaborations:** shaheerawan001@gmail.com \n\ud83d\udd17 **LinkedIn:** www.linkedin.com/in/muhammad-shaheer-898513192\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "864a4e02-d823-4846-bb13-9051420aba8e",
"connections": {
"Code": {
"main": [
[
{
"node": "Google Sheets",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Split Out": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
},
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"SERPAPI- GOOGE SEARCH": {
"main": [
[
{
"node": "Split Out",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Test workflow\u2019": {
"main": [
[
{
"node": "SERPAPI- GOOGE SEARCH",
"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.
googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow helps you automatically collect verified business leads from Google Search using SerpAPI — no coding required. It extracts company names, websites, emails, and phone numbers directly from search results and saves them into Google Sheets for easy follow-up or CRM…
Source: https://n8n.io/workflows/10657/ — 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 template automates the enrichment of business leads from a Google Sheet by: Triggering when a row is activated Searching for company information with Serper.dev Generating and validating potentia
The automation starts by retreiving the unused queries from a sheet, executes queries in the web using Serper API and extracts linkedin profiles of decision makers.
This workflow automates comprehensive SEO reporting by: Extracting keyword rankings and page performance from Google Search Console. Gathering organic reach metrics from Google Analytics. Analyzing in
SEO agencies doing competitor analysis for clients Content teams planning content strategies Marketing teams tracking competitive performance SEO professionals measuring AI search visibility
Stop losing warm leads in the noise. This automation analyzes your lead engagement data, calculates priority scores based on activity and last contact date, and automatically queues your top 10 leads