{
  "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[![Watch on YouTube](https://img.youtube.com/vi/gr9_wEMc9sM/hqdefault.jpg)](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
          }
        ]
      ]
    }
  }
}