{
  "name": "Lead Generation Flow",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -2080,
        256
      ],
      "id": "f72f7271-3c3e-41ae-9fae-0593b87fc408",
      "name": "Manual Trigger",
      "notes": "Click to start the workflow manually"
    },
    {
      "parameters": {
        "operation": "create",
        "documentId": {
          "__rl": true,
          "value": "1nfFNvbdD1dyUNWxRzuWiTzAODSxH8j5B06z_YKvOFg4",
          "mode": "list",
          "cachedResultName": "Leads",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1nfFNvbdD1dyUNWxRzuWiTzAODSxH8j5B06z_YKvOFg4/edit?usp=drivesdk"
        },
        "title": "={{ $now.toFormat(\"yyyy-MM-dd HH:mm\") }}",
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -1872,
        256
      ],
      "id": "70ab6843-92be-4846-8b6b-0569fe090f4f",
      "name": "Create Results Sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "notes": "Creates a new sheet with timestamp to store results"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "={{ $json.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "config",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -1664,
        256
      ],
      "id": "21f6bec3-a62f-4216-9484-661b5f1ab6e2",
      "name": "Read Config Sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "notes": "Reads search queries and parameters from config sheet"
    },
    {
      "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;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1456,
        256
      ],
      "id": "5254f5c9-0c16-4ebf-b199-3717dc823084",
      "name": "Build Search Tasks",
      "notes": "Converts config into multiple search tasks with pagination (20 results per page)"
    },
    {
      "parameters": {
        "resource": "google_maps",
        "q": "={{ $json.leads_query }} {{ $json.location }}",
        "additionalFields": {
          "start": "={{ $json.pageOffset }}",
          "ll": "=@40.7455096,-74.0083012,14z"
        }
      },
      "type": "@hasdata/n8n-nodes-hasdata.hasData",
      "typeVersion": 1,
      "position": [
        -1248,
        256
      ],
      "id": "59a2cb00-5e6a-4e18-b133-40b5e499820c",
      "name": "Search Google Maps",
      "retryOnFail": true,
      "credentials": {
        "hasDataApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "notes": "Fetches business listings from Google Maps based on query and location"
    },
    {
      "parameters": {
        "fieldToSplitOut": "localResults",
        "include": "=",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        -1040,
        256
      ],
      "id": "502d0020-9115-4e94-94ed-0005af31664f",
      "name": "Split Results",
      "notes": "Separates each business into individual items for processing"
    },
    {
      "parameters": {
        "compare": "selectedFields",
        "fieldsToCompare": "localResults.title, localResults.kgmid",
        "options": {}
      },
      "type": "n8n-nodes-base.removeDuplicates",
      "typeVersion": 2,
      "position": [
        -832,
        256
      ],
      "id": "93b5fee9-d26f-49c3-a7c0-8edce026ef3e",
      "name": "Remove Duplicates",
      "notes": "Removes duplicate businesses based on name and address"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "d2c86a6c-2c9d-472e-a328-71d92bdc74f2",
              "leftValue": "={{ $json.localResults.website }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -624,
        256
      ],
      "id": "6baaac69-71d4-440f-9aa3-2dec01bd4538",
      "name": "Has Website?",
      "notes": "Checks if business has a website - TRUE path scrapes it, FALSE path searches Google"
    },
    {
      "parameters": {
        "resource": "web_scraping",
        "url": "={{ $json.localResults.website }}",
        "additionalFields": {
          "extractEmails": true
        }
      },
      "type": "@hasdata/n8n-nodes-hasdata.hasData",
      "typeVersion": 1,
      "position": [
        -416,
        160
      ],
      "id": "394e8937-6a21-4036-a142-25fc3b79a81c",
      "name": "Scrape Website",
      "retryOnFail": true,
      "credentials": {
        "hasDataApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput",
      "notes": "Scrapes the business website to extract email addresses"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "5590ef2a-7d53-4637-afa0-10acdeb7dc41",
              "leftValue": "={{ $json.emails }}",
              "rightValue": "",
              "operator": {
                "type": "array",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -208,
        96
      ],
      "id": "6885fbf9-0e3d-44ed-8fb4-584dfd17c201",
      "name": "Emails Found?",
      "notes": "Checks if emails were found on website - TRUE adds them, FALSE searches Google"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a574f281-57df-4ee8-9d56-945f141b5345",
              "name": "title",
              "value": "={{ $('Has Website?').item.json.localResults.title }}",
              "type": "string"
            },
            {
              "id": "f92a90cd-b7ea-4740-8d0e-739d18912fa4",
              "name": "phone",
              "value": "={{ $('Has Website?').item.json.localResults.phone }}",
              "type": "string"
            },
            {
              "id": "c6a92d26-418d-41f0-86d1-4fe67755bf94",
              "name": "address",
              "value": "={{ $('Has Website?').item.json.localResults.address }}",
              "type": "string"
            },
            {
              "id": "5a1e6e7c-559f-4419-9719-73de143ba76a",
              "name": "website",
              "value": "={{ $('Has Website?').item.json.localResults.website }}",
              "type": "string"
            },
            {
              "id": "3ebc4a23-0bb6-43a0-b8f1-804acc29793b",
              "name": "emails",
              "value": "={{ $json.emails.join(', ') }}",
              "type": "string"
            },
            {
              "id": "03dd5a32-cc01-4cab-872b-8d18a40797cc",
              "name": "rating",
              "value": "={{ $('Has Website?').item.json.localResults.rating }}",
              "type": "number"
            },
            {
              "id": "361d3f7a-bfd4-40a7-901c-13e8e895d1fa",
              "name": "reviews",
              "value": "={{ $('Has Website?').item.json.localResults.reviews }}",
              "type": "number"
            },
            {
              "id": "b1a783ca-5559-4e2d-8d28-5b98140cc89d",
              "name": "type",
              "value": "={{ $('Has Website?').item.json.localResults.types.join(', ') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        0,
        0
      ],
      "id": "5a80f8c2-8169-4c06-9a14-b6cca319fc96",
      "name": "Format Lead with Emails",
      "notes": "Prepares lead data including scraped emails for output"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ad7fae25-b7e0-46ab-ad95-568ab9c6813a",
              "name": "localResults.title",
              "value": "={{ $('Has Website?').item.json.localResults.title }}",
              "type": "string"
            },
            {
              "id": "183dca66-d14a-4a63-9c14-30fffdaf4745",
              "name": "localResults.phone",
              "value": "={{ $('Has Website?').item.json.localResults.phone }}",
              "type": "string"
            },
            {
              "id": "d2791f5a-9e15-4d29-b1a7-488137c0deb8",
              "name": "localResults.address",
              "value": "={{ $('Has Website?').item.json.localResults.address }}",
              "type": "string"
            },
            {
              "id": "0d935b92-cb1e-4779-bd7e-3af1d2dd38ba",
              "name": "localResults.website",
              "value": "={{ $('Has Website?').item.json.localResults.website }}",
              "type": "string"
            },
            {
              "id": "faff970a-cb9d-4511-abb3-c97908cfdf59",
              "name": "rating",
              "value": "={{ $('Has Website?').item.json.localResults.rating }}",
              "type": "number"
            },
            {
              "id": "f4252269-c3b3-4ee2-a45c-458d3021123d",
              "name": "reviews",
              "value": "={{ $('Has Website?').item.json.localResults.reviews }}",
              "type": "number"
            },
            {
              "id": "4cc0b0f7-e726-4c69-8682-c87a75306efb",
              "name": "type",
              "value": "={{ $('Has Website?').item.json.localResults.types.join(', ') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        0,
        160
      ],
      "id": "578e651f-d067-44be-860a-80473fc5510e",
      "name": "Format Lead without Emails",
      "notes": "Prepares lead data without emails (will search Google next)"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        -368,
        400
      ],
      "id": "1033c1b5-9a71-4049-99ef-302cacc5a748",
      "name": "Merge for Google Search",
      "notes": "Combines leads without website or without scraped emails for Google search"
    },
    {
      "parameters": {
        "q": "={{ $json.localResults.title }}, {{ $json.localResults.address.split(', ').slice(1).join(', ') }} email \"@\"",
        "additionalFields": {}
      },
      "type": "@hasdata/n8n-nodes-hasdata.hasData",
      "typeVersion": 1,
      "position": [
        -192,
        400
      ],
      "id": "493d6ab0-d7b0-4553-ac19-b0cb6a9a28b4",
      "name": "Search Google for Emails",
      "credentials": {
        "hasDataApi": {
          "name": "<your credential>"
        }
      },
      "notes": "Searches Google to find email addresses for businesses"
    },
    {
      "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 for Google Search\").itemMatching(i).json;\n    sourceData = ancestor.localResults || ancestor;\n    fullData = ancestor;\n  } catch (e) {\n    const fallback = $(\"Merge 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;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        16,
        400
      ],
      "id": "5ea07ca7-9c06-4f56-bc32-45856a647af9",
      "name": "Extract Emails from Results",
      "notes": "Extracts email addresses from Google search results using regex"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        400,
        208
      ],
      "id": "da35c842-1741-4c17-be24-56567d96d3b7",
      "name": "Merge All Leads",
      "notes": "Combines all leads (with website emails and Google-found emails)"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Create Results Sheet').first().json.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Create Results Sheet').first().json.title }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [
            {
              "id": "title",
              "displayName": "title",
              "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": "address",
              "displayName": "address",
              "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": "emails",
              "displayName": "emails",
              "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": "type",
              "displayName": "type",
              "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": [
        576,
        208
      ],
      "id": "d53997d8-d504-4475-a412-0ab59e57bf24",
      "name": "Save to Results Sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "notes": "Writes all found leads to the timestamped results sheet"
    },
    {
      "parameters": {
        "content": "## Requirements\n\n- Google Sheets file **\"Leads\"**\n- Sheet **\"config\"** with fields:\n  - leads_query\n  - location\n  - page_count\n  - offset\n- [HasData API](https://hasdata.com/) credentials ",
        "height": 224,
        "width": 400
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2096,
        -64
      ],
      "typeVersion": 1,
      "id": "b95f73c0-42a7-483d-b662-315ebabf8ded",
      "name": "Sticky Note"
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Create Results Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Results Sheet": {
      "main": [
        [
          {
            "node": "Read Config Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Config Sheet": {
      "main": [
        [
          {
            "node": "Build Search Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Search Tasks": {
      "main": [
        [
          {
            "node": "Search Google Maps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google Maps": {
      "main": [
        [
          {
            "node": "Split Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Results": {
      "main": [
        [
          {
            "node": "Remove Duplicates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicates": {
      "main": [
        [
          {
            "node": "Has Website?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Website?": {
      "main": [
        [
          {
            "node": "Scrape Website",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge for Google Search",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Scrape Website": {
      "main": [
        [
          {
            "node": "Emails Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Emails Found?": {
      "main": [
        [
          {
            "node": "Format Lead with Emails",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Lead without Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Lead with Emails": {
      "main": [
        [
          {
            "node": "Merge All Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Lead without Emails": {
      "main": [
        [
          {
            "node": "Merge for Google Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge for Google Search": {
      "main": [
        [
          {
            "node": "Search Google for Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google for Emails": {
      "main": [
        [
          {
            "node": "Extract Emails from Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Emails from Results": {
      "main": [
        [
          {
            "node": "Merge All Leads",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge All Leads": {
      "main": [
        [
          {
            "node": "Save to Results Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "5e34e393-a5f4-4c7a-9235-04891b8f2a1d",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "QfqmMXhPJidm7tdH",
  "tags": []
}