AutomationFlowsMarketing & Ads › Score Local Google Maps Leads with Scavio and Google Sheets

Score Local Google Maps Leads with Scavio and Google Sheets

ByScavio AI @scavio-ai on n8n.io

This workflow runs weekly to read “niche in city” searches from Google Sheets, uses Scavio to pull up to three pages of Google Maps results per query, filters and scores the businesses, and appends ranked leads (with contact details and Maps links) back to Google Sheets. Runs…

Cron / scheduled trigger★★★★☆ complexity13 nodesGoogle SheetsN8N Nodes Scavio
Marketing & Ads Trigger: Cron / scheduled Nodes: 13 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17237 — we link there as the canonical source.

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 →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs weekly to read “niche in city” searches from Google Sheets, uses Scavio to pull up to three pages of Google Maps results per query, filters and scores the businesses, and appends ranked leads (with contact details and Maps links) back to Google Sheets. Runs…

Source: https://n8n.io/workflows/17237/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

This workflow transforms your Meta Ads creatives into a rich dataset of actionable insights. It's designed for data-driven marketers, performance agencies, and analysts who want to move beyond basic m

Facebook Graph Api, HTTP Request, Google Sheets
Marketing & Ads

I created this workflow with great care to help you simplify your daily reporting routine. If you manage Meta Ads campaigns, you know how time-consuming it can be to open Ads Manager, filter data, and

Google Sheets, Facebook Graph Api, Gmail +1
Marketing & Ads

I built this workflow to remove the daily pain of Meta Ads reporting. If you manage multiple ad accounts, you know how time-consuming it is to open Ads Manager, export campaign data, clean spreadsheet

Google Sheets, HTTP Request, Telegram
Marketing & Ads

This workflow automatically monitors your Google Ads campaigns every day, analyzing performance with AI-powered scoring to identify scaling opportunities and catch issues before they drain your budget

Google Ads, Google Sheets, Slack +1
Marketing & Ads

Automatically search LinkedIn for prospects using Unipile's API, enrich each profile with full details, and sync leads into a Google Sheet CRM — runs daily on a schedule with rate limiting, deduplicat

HTTP Request, Google Sheets