{
  "name": "Find, score and deduplicate Google Maps leads into Google Sheets with Slack alerts",
  "nodes": [
    {
      "parameters": {
        "content": "## Find, score and deduplicate local business leads from Google Maps, then alert Slack about hot leads\n\n### Who's it for\nWeb design, SEO and marketing agencies, freelancers and local sales teams who prospect small businesses and want a fresh, ranked lead list every week instead of a raw export to sort by hand.\n\n### How it works\n1. Every Monday at 8:00 the Schedule Trigger starts the workflow (or run it by hand).\n2. The **Settings** node holds your Google Maps searches (e.g. `plumber in Austin, TX`), the country and the score thresholds.\n3. The **Google Maps Businesses** Actor on Apify (`plainrows/google-maps-businesses`) returns structured places: name, category, phone, website, rating, review count, claimed status, opening hours, Google Maps link.\n4. The workflow reads the leads already in your sheet and keeps only **new places** (by Google place ID), also removing places found by two searches.\n5. Each new lead gets a **score from 0 to 100**, a tier (hot / warm / cold), the **reasons** behind the score and a suggested **sales angle** (e.g. \"No website: offer a simple site\"; \"Unclaimed Google profile: offer to claim and complete it\"; \"Rating 3.6 on 41 reviews: offer review management\").\n6. New leads are appended to the `Leads` tab, and one Slack message lists this week's hot leads with phone and Maps link. No hot lead, no message.\n\n### Requirements\n- An Apify account (free plan available) and its API token.\n- A Google Sheets account and a spreadsheet with a tab named `Leads`.\n- A Slack workspace and the Slack credential in n8n.\n- The Apify node is the verified community node `@apify/n8n-nodes-apify` (available on n8n Cloud; on self-hosted n8n install it from Settings > Community nodes).\n\n### How to customize\n- Edit `searchQueries` (one per line), `country`, `maxPlacesPerQuery`, `hotScore` and `warmScore` in **Settings**.\n- Change the weights in the *Score new leads* Code node: each rule is one line with its points, reason and sales angle (built for agencies selling websites, Google profile and reputation services; adapt them to what you sell).\n- Replace Slack with Gmail or Microsoft Teams using `{{ $json.text }}` as the message, or add a CRM node (HubSpot, Pipedrive) after *Score new leads*.\n\nYou pay per place returned (see the Actor's Pricing tab); the Apify node caps each run with *Maximum Cost per Run*. Places already in your sheet are still returned by the Actor but are not added twice.",
        "height": 760,
        "width": 520
      },
      "id": "c0702ef3-2ca9-535f-ad44-fc16964e545e",
      "name": "Description",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -340
      ]
    },
    {
      "parameters": {
        "content": "### Setup (10 min)\n1. **Apify**: free account at apify.com, then add your Apify credential to *Run Google Maps Businesses*.\n2. **Google Sheets**: create a tab named `Leads` with the header `placeId` in A1 (the other columns are added automatically), then paste the spreadsheet URL in *Read saved leads* and *Append new leads*.\n3. **Slack**: connect your account and pick the channel in *Post hot leads to Slack*.\n4. Edit **Settings**, then run once by hand.",
        "height": 320,
        "width": 420,
        "color": 7
      },
      "id": "0a819b4b-6bf9-54f5-931e-842100211de7",
      "name": "Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        440,
        -340
      ]
    },
    {
      "parameters": {
        "content": "### Lead score (0-100)\n- No website: +35\n- Unclaimed Google profile: +20\n- Rating under 4 (5+ reviews): +15\n- Fewer than 15 reviews: +10\n- No opening hours: +5\n- Phone listed: +15\n\nHot ≥ `hotScore` (50), warm ≥ `warmScore` (30). Edit the rules in *Score new leads*.",
        "height": 320,
        "width": 340,
        "color": 5
      },
      "id": "fed7dc97-c802-5054-b184-499b6b53d1a0",
      "name": "Scoring",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        900,
        -340
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "1d678152-8520-5a76-a9d8-065432bd345d",
      "name": "Every Monday 8:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        120
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "81f4450d-832b-561e-bd2b-0b1c6ebfa01b",
              "name": "searchQueries",
              "value": "plumber in Austin, TX\nelectrician in Austin, TX",
              "type": "string"
            },
            {
              "id": "85cba0f6-9549-567e-a986-4b7ed6f30979",
              "name": "country",
              "value": "US",
              "type": "string"
            },
            {
              "id": "e7cbde67-7d56-59f0-b7d6-4da24b6da322",
              "name": "maxPlacesPerQuery",
              "value": 50,
              "type": "number"
            },
            {
              "id": "367a3726-f822-597c-9952-19b234d6e0c7",
              "name": "hotScore",
              "value": 50,
              "type": "number"
            },
            {
              "id": "1c4a1963-665c-57be-97d3-64911a592a00",
              "name": "warmScore",
              "value": 30,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "eb9f85fb-20eb-5728-b2a7-76736018b951",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        120
      ]
    },
    {
      "parameters": {
        "resource": "Actors",
        "operation": "Run actor and get dataset",
        "actorId": "plainrows~google-maps-businesses",
        "customBody": "={{ JSON.stringify({ searchQueries: $json.searchQueries.split('\\n').map((q) => q.trim()).filter(Boolean), country: $json.country, maxPlacesPerQuery: Number($json.maxPlacesPerQuery) || 50 }) }}",
        "maxTotalChargeUsd": 5
      },
      "id": "6aa0103a-2670-5359-8cde-5ad836128b15",
      "name": "Run Google Maps Businesses",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        440,
        120
      ],
      "credentials": {
        "apifyApi": {
          "id": "",
          "name": "Apify account"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "Leads",
          "mode": "name"
        },
        "options": {}
      },
      "id": "c64f0413-ffc0-562b-b67c-c0964ab12f04",
      "name": "Read saved leads",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        120
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      },
      "executeOnce": true,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "// Keep places not already in the sheet, score them and explain the score.\nconst settings = $('Settings').first().json;\nconst hotScore = Number(settings.hotScore) || 50;\nconst warmScore = Number(settings.warmScore) || 30;\nconst known = new Set($('Read saved leads').all().map((i) => String(i.json.placeId ?? '')).filter(Boolean));\nconst today = new Date().toISOString().slice(0, 10);\n\n// Each rule: [applies?, points, reason, sales angle]. Adapt the weights to what you sell.\nconst rules = (p) => [\n  [!p.website, 35, 'No website', 'Offer a simple website'],\n  [p.isClaimed === false, 20, 'Unclaimed Google profile', 'Offer to claim and complete the Google profile'],\n  [p.rating != null && p.rating < 4 && p.reviewsCount >= 5, 15, `Rating ${p.rating} on ${p.reviewsCount} reviews`, 'Offer review management'],\n  [p.reviewsCount != null && p.reviewsCount < 15, 10, `Only ${p.reviewsCount} reviews`, 'Offer a review campaign'],\n  [!p.openingHours, 5, 'No opening hours', 'Offer to claim and complete the Google profile'],\n  [!!p.phone, 15, 'Phone listed', null],\n];\n\nconst seen = new Set();\nconst leads = [];\nfor (const { json: p } of $('Run Google Maps Businesses').all()) {\n  if (p.found === false || !p.placeId || known.has(p.placeId) || seen.has(p.placeId)) continue;\n  seen.add(p.placeId);\n  const hits = rules(p).filter(([applies]) => applies);\n  const score = Math.min(100, hits.reduce((sum, [, points]) => sum + points, 0));\n  const angles = [...new Set(hits.map(([, , , angle]) => angle).filter(Boolean))];\n  leads.push({\n    json: {\n      placeId: p.placeId,\n      name: p.name,\n      leadScore: score,\n      tier: score >= hotScore ? 'hot' : score >= warmScore ? 'warm' : 'cold',\n      reasons: hits.map(([, , reason]) => reason).join('; '),\n      salesAngle: angles.join('; '),\n      category: p.category,\n      phone: p.phone,\n      website: p.website,\n      rating: p.rating,\n      reviewsCount: p.reviewsCount,\n      isClaimed: p.isClaimed,\n      address: p.address,\n      city: p.city,\n      googleMapsUrl: p.googleMapsUrl,\n      searchQuery: p.searchQuery,\n      dateAdded: today,\n    },\n  });\n}\nreturn leads.sort((a, b) => b.json.leadScore - a.json.leadScore);"
      },
      "id": "b04f9bda-7f17-514c-9e66-da6f662b3564",
      "name": "Score new leads",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        120
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "url"
        },
        "sheetName": {
          "__rl": true,
          "value": "Leads",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "56209dc0-ef90-58d0-b61b-68b845974c15",
      "name": "Append new leads",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1100,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// One Slack message with this run's hot leads (best first). No hot lead: no message.\nconst hot = $input.all().map((i) => i.json).filter((l) => l.tier === 'hot');\nif (hot.length === 0) return [];\nconst shown = hot.slice(0, 15).map((l) =>\n  `• *${l.name}* (${l.category ?? 'business'}, ${l.city ?? ''}) score ${l.leadScore}\\n` +\n  `   ${l.reasons}\\n   :bulb: ${l.salesAngle || '-'}\\n` +\n  `   ${l.phone ?? 'no phone'} | <${l.googleMapsUrl}|Google Maps>`);\nconst more = hot.length > shown.length ? `\\n…and ${hot.length - shown.length} more in the sheet.` : '';\nreturn [{ json: { text: `:dart: *${hot.length} new hot lead${hot.length > 1 ? 's' : ''}* from Google Maps\\n\\n${shown.join('\\n\\n')}${more}`, hotLeads: hot.length } }];"
      },
      "id": "ac24a99a-0fd2-556b-ad74-436a14d1ed5f",
      "name": "Build hot leads message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        240
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "",
          "mode": "list"
        },
        "text": "={{ $json.text }}",
        "otherOptions": {}
      },
      "id": "5867672e-6a84-5f73-94e1-0ab2c9268bfb",
      "name": "Post hot leads to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1320,
        240
      ],
      "credentials": {
        "slackApi": {
          "id": "",
          "name": "Slack account"
        }
      }
    }
  ],
  "connections": {
    "Every Monday 8:00": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Run Google Maps Businesses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Google Maps Businesses": {
      "main": [
        [
          {
            "node": "Read saved leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read saved leads": {
      "main": [
        [
          {
            "node": "Score new leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score new leads": {
      "main": [
        [
          {
            "node": "Append new leads",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build hot leads message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build hot leads message": {
      "main": [
        [
          {
            "node": "Post hot leads to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}
