{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Local Lead Finder with Scavio + Google Sheets",
  "nodes": [
    {
      "id": "sticky-master",
      "name": "How it works",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        180,
        160
      ],
      "parameters": {
        "width": 580,
        "height": 760,
        "content": "## Local Lead Finder with Scavio + Google Sheets\n\n### How it works\nFind local businesses you can actually sell to. For each \"niche in city\" search in your sheet, this workflow pulls three pages of Google Maps results through [Scavio](https://scavio.dev), throws away anything outside the city you asked for, then scores every business on how much it needs your help.\n\nThe score is the point: **no website +40**, **rating under 4.0 +25**, **fewer than 20 reviews +20**, **no phone +15**. A business with no website and 6 reviews scores 60 and lands at the top of your sheet. These are the shops that need a website, better reviews, or ads -- and they are findable today.\n\nEvery lead is written with its phone, address, category, and a Google Maps link, so the sheet is a call list, not a research project.\n\n### Setup steps\n- [ ] Install the **Scavio** community node: Settings -> Community Nodes -> install `n8n-nodes-scavio` (version 0.4.0 or later).\n- [ ] Get a free Scavio API key at https://dashboard.scavio.dev/sign-up (50 free credits).\n- [ ] Create a Google Sheet with a tab **Searches** (column `query`) and a tab **Leads**.\n- [ ] Add one search per row in the form `niche in city`, e.g. `plumbers in Austin, TX`.\n- [ ] Attach credentials: Scavio API on all three Maps nodes, Google Sheets OAuth2 on **Read searches** and **Append leads**.\n- [ ] Pick your spreadsheet and tabs in the two Google Sheets nodes.\n- [ ] Activate the workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-a",
      "name": "Cluster: Read searches",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        820,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 620,
        "height": 320,
        "content": "## 1. Read searches\n\nRuns weekly and reads every \"niche in city\" query from the \"Searches\" tab, one at a time."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-b",
      "name": "Cluster: Three pages of Maps",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1480,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 840,
        "height": 320,
        "content": "## 2. Three pages of Maps\n\nA 2s throttle, then three Google Maps searches at offsets 0, 20 and 40 -- up to 60 businesses per query."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-c",
      "name": "Cluster: Score and log",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2360,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 320,
        "content": "## 3. Score and log\n\nFilters to the city you asked for, de-duplicates across pages, scores each business, and appends the ranked leads."
      },
      "typeVersion": 1
    },
    {
      "id": "1",
      "name": "Every Week",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        860,
        340
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "2",
      "name": "Read searches",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1080,
        340
      ],
      "parameters": {
        "options": {},
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "3",
      "name": "Loop searches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1300,
        340
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "4",
      "name": "Throttle",
      "type": "n8n-nodes-base.wait",
      "position": [
        1520,
        340
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1.1
    },
    {
      "id": "5",
      "name": "Maps page 1",
      "type": "n8n-nodes-scavio.scavio",
      "position": [
        1740,
        340
      ],
      "parameters": {
        "query": "={{ $('Loop searches').item.json.query }}",
        "resource": "google",
        "operation": "mapsSearch",
        "additionalOptions": {
          "gl": "us"
        }
      },
      "credentials": {
        "scavioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "6",
      "name": "Maps page 2",
      "type": "n8n-nodes-scavio.scavio",
      "position": [
        1960,
        340
      ],
      "parameters": {
        "query": "={{ $('Loop searches').item.json.query }}",
        "resource": "google",
        "operation": "mapsSearch",
        "additionalOptions": {
          "gl": "us",
          "start": 20
        }
      },
      "credentials": {
        "scavioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "7",
      "name": "Maps page 3",
      "type": "n8n-nodes-scavio.scavio",
      "position": [
        2180,
        340
      ],
      "parameters": {
        "query": "={{ $('Loop searches').item.json.query }}",
        "resource": "google",
        "operation": "mapsSearch",
        "additionalOptions": {
          "gl": "us",
          "start": 40
        }
      },
      "credentials": {
        "scavioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "8",
      "name": "Score leads",
      "type": "n8n-nodes-base.code",
      "position": [
        2400,
        340
      ],
      "parameters": {
        "jsCode": "function pick(obj, path) {\n  return String(path).split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);\n}\nfunction num(...vals) {\n  for (const v of vals) {\n    if (v == null) continue;\n    const n = typeof v === 'string' ? Number(v) : v;\n    if (typeof n === 'number' && Number.isFinite(n)) return n;\n  }\n  return undefined;\n}\nfunction str(v) {\n  return typeof v === 'string' ? v : v == null ? '' : String(v);\n}\nfunction deepFind(root, match, depth) {\n  depth = depth || 0;\n  if (root == null || depth > 6 || typeof root !== 'object') return undefined;\n  if (!Array.isArray(root) && match(root)) return root;\n  for (const v of Object.values(root)) {\n    if (v && typeof v === 'object') {\n      const f = deepFind(v, match, depth + 1);\n      if (f) return f;\n    }\n  }\n  return undefined;\n}\nfunction deepFindArray(root, match, depth) {\n  depth = depth || 0;\n  if (root == null || depth > 6 || typeof root !== 'object') return undefined;\n  if (Array.isArray(root)) {\n    if (root.length && root.some((el) => el && typeof el === 'object' && match(el))) return root;\n  }\n  for (const v of Object.values(root)) {\n    if (v && typeof v === 'object') {\n      const f = deepFindArray(v, match, depth + 1);\n      if (f) return f;\n    }\n  }\n  return undefined;\n}\nfunction unwrap(raw) {\n  if (raw && typeof raw === 'object') {\n    if (raw.node && typeof raw.node === 'object') return raw.node;\n    if (raw.media && typeof raw.media === 'object') return raw.media;\n  }\n  return raw || {};\n}\nfunction isoFrom(ts) {\n  const t = num(ts);\n  if (t == null) return '';\n  return new Date(t > 1e12 ? t : t * 1000).toISOString();\n}\n\n// Lead scoring and market filtering, exactly as the Scavio dashboard tool does it.\nconst query = str($('Loop searches').first().json.query).trim();\n\n// Market tokens = everything after a standalone \"in\", lowercased and split.\n// \"plumbers in Austin, TX\" -> [\"austin\", \"tx\"]\nfunction marketTokens(q) {\n  const m = q.match(/\\bin\\s+(.+)$/i);\n  if (!m) return [];\n  return m[1].split(/[, ]+/).map((s) => s.trim().toLowerCase()).filter((s) => s.length >= 2);\n}\nconst tokens = marketTokens(query);\n\n// A lead with no address is dropped when a market is specified -- same as the dashboard.\nfunction inMarket(b) {\n  if (tokens.length === 0) return true;\n  const addr = str(b.address).toLowerCase();\n  return !!addr && tokens.some((t) => addr.includes(t));\n}\n\nfunction scoreLead(b) {\n  const reasons = [];\n  let score = 0;\n  const rating = num(b.rating);\n  const reviews = num(b.reviews);\n  if (!b.website) { score += 40; reasons.push('no website'); }\n  if (rating != null && rating < 4.0) { score += 25; reasons.push('low rating (' + rating + ')'); }\n  if (reviews != null && reviews < 20) { score += 20; reasons.push('few reviews (' + reviews + ')'); }\n  if (!b.phone) { score += 15; reasons.push('no phone'); }\n  return { score: score, reasons: reasons };\n}\n\nconst pages = ['Maps page 1', 'Maps page 2', 'Maps page 3'];\nconst byKey = new Map();\n\nfor (const pageName of pages) {\n  let raw;\n  try { raw = $(pageName).first().json; } catch (e) { raw = undefined; }\n  const list = (raw && raw.local_results) || pick(raw, 'data.local_results') || [];\n  if (!Array.isArray(list) || list.length === 0) continue;\n  for (const b of list) {\n    if (!b || typeof b !== 'object') continue;\n    // First occurrence wins, across all pages.\n    const key = str(b.place_id) || str(b.data_id) || str(b.title);\n    if (!key || byKey.has(key)) continue;\n    if (!inMarket(b)) continue;\n    const scored = scoreLead(b);\n    byKey.set(key, {\n      query: query,\n      foundAt: new Date().toISOString().slice(0, 10),\n      name: str(b.title),\n      address: str(b.address),\n      phone: str(b.phone),\n      website: str(b.website),\n      rating: num(b.rating) ?? '',\n      reviews: num(b.reviews) ?? '',\n      category: str(b.type),\n      score: scored.score,\n      opportunities: scored.reasons.join('; '),\n      mapsUrl: b.place_id ? 'https://www.google.com/maps/place/?q=place_id:' + str(b.place_id) : '',\n    });\n  }\n}\n\nconst leads = [...byKey.values()].sort((a, b) => b.score - a.score);\n\nif (leads.length === 0) {\n  return [];\n}\n\nreturn leads.map((l) => ({ json: l }));",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "9",
      "name": "Append leads",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2620,
        340
      ],
      "parameters": {
        "columns": {
          "value": {
            "name": "={{ $json.name }}",
            "phone": "={{ $json.phone }}",
            "query": "={{ $json.query }}",
            "score": "={{ $json.score }}",
            "rating": "={{ $json.rating }}",
            "address": "={{ $json.address }}",
            "foundAt": "={{ $json.foundAt }}",
            "mapsUrl": "={{ $json.mapsUrl }}",
            "reviews": "={{ $json.reviews }}",
            "website": "={{ $json.website }}",
            "category": "={{ $json.category }}",
            "opportunities": "={{ $json.opportunities }}"
          },
          "schema": [
            {
              "id": "foundAt",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "foundAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "query",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "query",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "address",
              "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": "rating",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviews",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reviews",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "opportunities",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "opportunities",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "mapsUrl",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "mapsUrl",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Throttle": {
      "main": [
        [
          {
            "node": "Maps page 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Week": {
      "main": [
        [
          {
            "node": "Read searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Maps page 1": {
      "main": [
        [
          {
            "node": "Maps page 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Maps page 2": {
      "main": [
        [
          {
            "node": "Maps page 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Maps page 3": {
      "main": [
        [
          {
            "node": "Score leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score leads": {
      "main": [
        [
          {
            "node": "Append leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append leads": {
      "main": [
        [
          {
            "node": "Loop searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop searches": {
      "main": [
        [],
        [
          {
            "node": "Throttle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read searches": {
      "main": [
        [
          {
            "node": "Loop searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}