{
  "id": "dgMapsLeadsBatch1",
  "name": "Google Maps leads to contact details to Google Sheet (batch, one contact run)",
  "nodes": [
    {
      "parameters": {},
      "id": "a2000000-0000-4000-8000-000000000001",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "findLocalLeads",
        "searches": "plumber in Austin, TX",
        "maxPlacesPerSearch": 10,
        "includeWebsiteContacts": false,
        "includeDecisionMakers": false,
        "leadOptions": {
          "onlyWithWebsite": true
        }
      },
      "id": "a2000000-0000-4000-8000-000000000002",
      "name": "Find Local Leads",
      "type": "n8n-nodes-datagleaner.dataGleaner",
      "typeVersion": 1,
      "position": [
        240,
        0
      ],
      "credentials": {
        "dataGleanerApi": {
          "id": "",
          "name": "Data Gleaner (Apify) API"
        }
      },
      "notesInFlow": true,
      "notes": "Edit the search (one per line, category plus city). Max Places Per Search is billed per place, so it is your cost cap. Website contacts are off here because the next node does that step."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Collect every place website into one newline-separated list so the next node makes ONE Get Contact Details run for the whole search.\n// Social-network pages (Facebook, Instagram, LinkedIn, X, TikTok, YouTube, Linktree) are not company websites, so they are skipped.\nconst social = /^(?:[a-z][a-z0-9+.-]*:\\/\\/)?(?:[^\\/@?#]*@)?(?:[^\\/:?#]+\\.)?(facebook\\.com|fb\\.com|fb\\.me|instagram\\.com|linkedin\\.com|x\\.com|twitter\\.com|tiktok\\.com|youtube\\.com|youtu\\.be|linktr\\.ee)(?:[:\\/?#]|$)/i;\nconst seen = new Set();\nconst sites = [];\nfor (const item of $input.all()) {\n  const w = (item.json.website || '').trim();\n  if (w && !social.test(w) && !seen.has(w)) { seen.add(w); sites.push(w); }\n}\nreturn [{ json: { websites: sites.join('\\n'), count: sites.length } }];"
      },
      "id": "a2000000-0000-4000-8000-000000000005",
      "name": "Collect Websites",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        0
      ],
      "notesInFlow": true,
      "notes": "Turns N places into one item holding all their websites, so Get Contact Details runs once instead of N times."
    },
    {
      "parameters": {
        "operation": "getContactDetails",
        "websites": "={{ $json.websites }}",
        "contactOptions": {
          "maxPagesPerSite": 4
        }
      },
      "id": "a2000000-0000-4000-8000-000000000003",
      "name": "Get Contact Details",
      "type": "n8n-nodes-datagleaner.dataGleaner",
      "typeVersion": 1,
      "position": [
        600,
        0
      ],
      "credentials": {
        "dataGleanerApi": {
          "id": "",
          "name": "Data Gleaner (Apify) API"
        }
      },
      "onError": "continueRegularOutput",
      "notesInFlow": true,
      "notes": "One run for all websites in the search. A failed site comes back with its own status instead of stopping the workflow."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Contact results come back in a different order than the input, so match each place to its contact row by website host.\nconst host = (u) => { const m = String(u || '').match(/^(?:[a-z][a-z0-9+.-]*:\\/\\/)?([^\\/?#:]+)/i); return m ? m[1].replace(/^www\\./i, '').toLowerCase() : ''; };\nconst social = /^(?:[a-z][a-z0-9+.-]*:\\/\\/)?(?:[^\\/@?#]*@)?(?:[^\\/:?#]+\\.)?(facebook\\.com|fb\\.com|fb\\.me|instagram\\.com|linkedin\\.com|x\\.com|twitter\\.com|tiktok\\.com|youtube\\.com|youtu\\.be|linktr\\.ee)(?:[:\\/?#]|$)/i;\nconst byHost = {};\nfor (const item of $input.all()) {\n  const r = item.json;\n  const h = host(r.website) || host(r.finalUrl) || (r.domain || '').toLowerCase();\n  if (!h) continue;\n  if (!byHost[h] || ((r.emailList || []).length > (byHost[h].emailList || []).length)) byHost[h] = r;\n}\nconst out = [];\nfor (const item of $('Find Local Leads').all()) {\n  const p = item.json;\n  const c = byHost[host(p.website)] || {};\n  out.push({ json: {\n    placeId: p.placeId, name: p.name, address: p.address, phone: p.phone, website: p.website,\n    emails: (c.emailList || []).join(', '),\n    linkedin: (c.linkedin || []).join(', '),\n    facebook: (c.facebook || []).join(', '),\n    contactStatus: c.status || (p.website && social.test(String(p.website).trim()) ? 'skipped (social URL)' : 'no result'),\n  } });\n}\nreturn out;"
      },
      "id": "a2000000-0000-4000-8000-000000000006",
      "name": "Match Contacts to Places",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        840,
        0
      ],
      "notesInFlow": true,
      "notes": "Joins the single batch result back onto each place by website host."
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "placeId": "={{ $json.placeId }}",
            "name": "={{ $json.name }}",
            "address": "={{ $json.address }}",
            "phone": "={{ $json.phone }}",
            "website": "={{ $json.website }}",
            "emails": "={{ $json.emails }}",
            "linkedin": "={{ $json.linkedin }}",
            "facebook": "={{ $json.facebook }}"
          },
          "matchingColumns": [
            "placeId"
          ],
          "schema": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {}
      },
      "id": "a2000000-0000-4000-8000-000000000004",
      "name": "Write to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1080,
        0
      ],
      "notesInFlow": true,
      "notes": "Pick your spreadsheet and tab. Row 1 needs the headers: placeId, name, address, phone, website, emails, linkedin, facebook. Rows match on placeId, so reruns update instead of duplicating."
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Find Local Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Local Leads": {
      "main": [
        [
          {
            "node": "Collect Websites",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Websites": {
      "main": [
        [
          {
            "node": "Get Contact Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Contact Details": {
      "main": [
        [
          {
            "node": "Match Contacts to Places",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Contacts to Places": {
      "main": [
        [
          {
            "node": "Write to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
