AutomationFlowsMarketing & Ads › Find and Save Local Lead Prospects with Google Places, Firecrawl and Supabase

Find and Save Local Lead Prospects with Google Places, Firecrawl and Supabase

ByPedro Olavarria @polavarria on n8n.io

This workflow receives a webhook request with a target business vertical and location, finds matching companies via Google Places API, scrapes their websites with Firecrawl to extract and validate contact emails, deduplicates against Supabase, stores new prospects, and sends a…

Webhook trigger★★★★☆ complexity24 nodesHTTP RequestMicrosoft Outlook
Marketing & Ads Trigger: Webhook Nodes: 24 Complexity: ★★★★☆ Added:

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

This workflow follows the HTTP Request → Microsoft Outlook recipe pattern — see all workflows that pair these two integrations.

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
{
  "name": "Prospecting Engine with Google Places API",
  "nodes": [
    {
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        304,
        -64
      ],
      "parameters": {
        "path": "your-webhook-path",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "name": "Set Config",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        -64
      ],
      "parameters": {
        "jsCode": "const b = $json.body || {};\nconst vertical = String(b.vertical || 'HVAC contractor').trim();\nconst location = String(b.location || 'Orlando, Florida').trim();\nconst limit = Math.min(parseInt(b.limit) || 20, 20);\nreturn [{ json: { vertical, location, limit, textQuery: `${vertical} in ${location}` } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Places Text Search",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        656,
        -64
      ],
      "parameters": {
        "url": "https://places.googleapis.com/v1/places:searchText",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ textQuery: $json.textQuery, maxResultCount: $json.limit }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "X-Goog-FieldMask",
              "value": "places.id,places.displayName,places.websiteUri,places.nationalPhoneNumber,places.formattedAddress,places.businessStatus,places.primaryTypeDisplayName"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "name": "Map Businesses",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        -64
      ],
      "parameters": {
        "jsCode": "const resp = $input.first().json;\nconst places = (resp && resp.places) || [];\n// Exclude YourCo's own competitors (IT / AI / automation / software / digital-agency / MSP shops)\nconst COMPETITOR = /\\b(it services|it support|information technology|managed (it|services?)|msp|software|saas|web (design|develop|developer|development)|app (design|develop|developer|development)|mobile app|digital marketing|marketing agency|advertising agency|seo|artificial intelligence|a\\.?i\\.?|automation|computer (repair|services?)|tech(nology)? (support|solutions?|services?|consult\\w*)|cyber\\s?security|cloud (services?|solutions?)|data (analytics|science)|consulting (firm|group|services))\\b/i;\nconst isComp = (n,cat)=>COMPETITOR.test(((n||'')+' '+(cat||'')).toLowerCase());\nreturn places\n  .filter(p => p.businessStatus === 'OPERATIONAL' && p.websiteUri)\n  .filter(p => !isComp((p.displayName&&p.displayName.text)||'', (p.primaryTypeDisplayName&&p.primaryTypeDisplayName.text)||''))\n  .map(p => ({ json: {\n    business_name: (p.displayName && p.displayName.text) || '',\n    website_url: p.websiteUri || '',\n    phone: p.nationalPhoneNumber || '',\n    address: p.formattedAddress || '',\n    place_id: p.id || '',\n    category: (p.primaryTypeDisplayName && p.primaryTypeDisplayName.text) || '',\n    vertical: $('Set Config').first().json.vertical,\n    location: $('Set Config').first().json.location,\n  }}));"
      },
      "typeVersion": 2
    },
    {
      "name": "Respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1440,
        336
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, vertical: $('Map + Dedup').first().json.vertical, location: $('Map + Dedup').first().json.location, total_found: $('Map + Dedup').first().json.total_found, total_emailable: $('Map + Dedup').first().json.total_emailable, fresh_inserted: $('Map + Dedup').first().json.fresh_inserted, duplicates: $('Map + Dedup').first().json.duplicates }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "name": "Firecrawl Scrape",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1136,
        -64
      ],
      "parameters": {
        "url": "https://api.firecrawl.dev/v1/scrape",
        "method": "POST",
        "options": {
          "timeout": 25000
        },
        "jsonBody": "={{ JSON.stringify({ url: $json.website_url, formats: ['markdown'], onlyMainContent: false, timeout: 20000 }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "firecrawlApi"
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "name": "Extract Email",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        -64
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const ctx = $('Find Contact').item.json;\nconst fc = $json || {};\nconst md = (fc.data && fc.data.markdown) || fc.markdown || '';\nconst parked = !!ctx.is_parked;\nlet ce = (md.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g) || [])\n  .map(e => e.toLowerCase())\n  .filter(e => !/\\.(png|jpe?g|gif|webp|svg|css|js)$/.test(e))\n  .filter(e => !/(example\\.com|sentry|wixpress|godaddy|squarespace|cloudflare|@2x)/.test(e));\nlet emails = [...new Set([...(ctx.homepage_emails || []), ...ce])];\nconst domain = String(ctx.website_url || '').replace(/^https?:\\/\\//, '').replace(/^www\\./, '').split('/')[0].toLowerCase();\nconst onDomain = emails.filter(e => domain && e.endsWith('@' + domain));\nconst role = emails.find(e => /^(info|contact|hello|office|admin|sales|frontdesk|reception|appointments|service|hi)@/.test(e));\nconst preferred = parked ? '' : (onDomain[0] || role || emails[0] || '');\nconst fromContact = ce.includes(preferred);\nreturn { json: {\n  business_name: ctx.business_name, website_url: ctx.website_url, phone: ctx.phone,\n  address: ctx.address, place_id: ctx.place_id, category: ctx.category,\n  vertical: ctx.vertical, location: ctx.location,\n  verified_email: preferred, all_emails: emails.slice(0, 5), is_parked: parked,\n  email_source_url: preferred ? (fromContact ? ctx.contact_url : ctx.website_url) : ctx.website_url,\n  content_fetch_status: parked ? 'parked' : ((ctx.homepage_md_len || md.length) ? 'ok' : 'no_content')\n} };"
      },
      "typeVersion": 2
    },
    {
      "name": "Collect Results",
      "type": "n8n-nodes-base.code",
      "position": [
        2288,
        -64
      ],
      "parameters": {
        "jsCode": "const all = $input.all().map(i => i.json);\nconst withEmail = all.filter(b => b.verified_email);\nreturn [{ json: { ok: true, vertical: all[0] && all[0].vertical, location: all[0] && all[0].location,\n  total: all.length, with_email: withEmail.length, businesses: all } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Find Contact",
      "type": "n8n-nodes-base.code",
      "position": [
        1312,
        -64
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const biz = $('Map Businesses').item.json;\nconst fc = $json || {};\nconst md = (fc.data && fc.data.markdown) || fc.markdown || '';\nconst lower = md.toLowerCase();\nconst is_parked = md.length < 150 || /this domain (is|may be|name)|domain( name)? for sale|buy this domain|get this domain|hugedomains|parked free|courtesy of (godaddy|the)|domain parking|sedoparking|domainmarket|this web ?page is parked|future home of|parked free of charge/.test(lower);\nlet he = (md.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g) || [])\n  .map(e => e.toLowerCase())\n  .filter(e => !/\\.(png|jpe?g|gif|webp|svg|css|js)$/.test(e))\n  .filter(e => !/(example\\.com|sentry|wixpress|godaddy|squarespace|cloudflare|@2x)/.test(e));\nconst cm = md.match(/\\]\\((https?:\\/\\/[^)]*contact[^)]*)\\)/i);\nlet contact_url = cm ? cm[1].split('#')[0] : '';\nif (!contact_url && biz.website_url) contact_url = biz.website_url.replace(/\\/$/, '') + '/contact';\nreturn { json: { ...biz, homepage_emails: [...new Set(he)], homepage_md_len: md.length, contact_url, is_parked } };"
      },
      "typeVersion": 2
    },
    {
      "name": "Firecrawl Contact",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1488,
        -64
      ],
      "parameters": {
        "url": "https://api.firecrawl.dev/v1/scrape",
        "method": "POST",
        "options": {
          "timeout": 25000
        },
        "jsonBody": "={{ JSON.stringify({ url: $json.contact_url, formats: ['markdown'], onlyMainContent: false, timeout: 20000 }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "firecrawlApi"
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "name": "Fetch Existing",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        304,
        336
      ],
      "parameters": {
        "url": "https://YOUR-PROJECT.supabase.co/rest/v1/pilot_prospects?select=website_url,verified_email&limit=5000",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "name": "Map + Dedup",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        336
      ],
      "parameters": {
        "jsCode": "const col = $('Collect Results').first().json;\nconst businesses = col.businesses || [];\nconst existing = $input.all().map(i => i.json);\nconst norm = u => String(u || '').toLowerCase().replace(/\\/$/, '');\nconst exUrls = new Set(existing.map(e => norm(e.website_url)));\nconst exEmails = new Set(existing.map(e => String(e.verified_email || '').toLowerCase()).filter(Boolean));\nconst emailable = businesses.filter(b => b.verified_email && b.mx_record_found && !b.is_parked);\nconst fresh = emailable.filter(b => !exUrls.has(norm(b.website_url)) && !exEmails.has(String(b.verified_email).toLowerCase()));\nconst uuid = () => { try { return crypto.randomUUID(); } catch (e) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } };\nconst pains = [\n  'Manual lead intake and follow-up that eats staff hours',\n  'Missed-call and after-hours inquiries turning into lost jobs',\n  'Manual quoting, scheduling, and reminders',\n  'Chasing invoices and payments by hand'\n];\nconst now = new Date().toISOString();\nconst prospects = fresh.map(b => ({\n  id: uuid(),\n  workflow_name: 'YourCo - Prospecting Engine v2', workflow_version: '2.0', pilot_state: 'discovered',\n  pilot_vertical: b.vertical, business_name: b.business_name, category: b.category,\n  city_or_region: b.location, website_url: b.website_url, verified_email: b.verified_email,\n  email_source_url: b.email_source_url, phone: b.phone, mx_record_found: !!b.mx_record_found,\n  pain_points: pains,\n  YourCo_fit_summary: `${b.vertical} in ${b.location} likely loses hours to manual intake, scheduling, and follow-up that AI automation can handle.`,\n  confidence_score: 0.7, send_recommendation: 'review', review_status: 'new', generated_at: now, received_at: now\n}));\nconst esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst reviewBase = 'https://YOUR-N8N-INSTANCE/webhook/YOUR-WEBHOOK-PATH';\nconst btn = (url, bg, label) => `<a href=\"${url}\" style=\"display:inline-block;padding:6px 14px;margin:0 3px;border-radius:5px;background:${bg};color:#ffffff;text-decoration:none;font-size:13px;font-weight:bold;\">${label}</a>`;\nconst rows = prospects.map(p => {\n  const site = esc(p.website_url);\n  const fire = btn(reviewBase + '?action=approve&id=' + p.id, '#16a34a', '\ud83d\udd25 Fire');\n  const deny = btn(reviewBase + '?action=reject&id=' + p.id, '#6b7280', 'Deny');\n  return `<tr><td style=\"padding:8px;border:1px solid #ddd;\"><strong>${esc(p.business_name)}</strong><br><a href=\"${site}\" style=\"color:#1B4F8A;font-size:12px;\">${site}</a></td><td style=\"padding:8px;border:1px solid #ddd;\"><a href=\"mailto:${esc(p.verified_email)}\">${esc(p.verified_email)}</a></td><td style=\"padding:8px;border:1px solid #ddd;\">${esc(p.phone)}</td><td style=\"padding:8px;border:1px solid #ddd;white-space:nowrap;\">${fire}${deny}</td></tr>`;\n}).join('');\nconst digest_html = `<div style=\"font-family:Arial,sans-serif;color:#1a1a1a;max-width:880px;\"><h2 style=\"color:#0B2545;margin:0 0 6px;\">${prospects.length} new ${esc(col.vertical)} prospects - ${esc(col.location)}</h2><p style=\"color:#555;\">Found ${businesses.length}, emailable+MX ${emailable.length}, NEW ${prospects.length}, dupes skipped ${emailable.length - fresh.length}. Parked/dead sites auto-filtered.</p>${prospects.length ? `<table style=\"border-collapse:collapse;font-size:14px;width:100%;\"><tr><th style=\"padding:8px;border:1px solid #ddd;background:#f4f6f9;text-align:left;\">Business</th><th style=\"padding:8px;border:1px solid #ddd;background:#f4f6f9;text-align:left;\">Email</th><th style=\"padding:8px;border:1px solid #ddd;background:#f4f6f9;text-align:left;\">Phone</th><th style=\"padding:8px;border:1px solid #ddd;background:#f4f6f9;text-align:left;\">Action</th></tr>${rows}</table>` : '<p>No new prospects this run.</p>'}<p style=\"color:#555;font-size:13px;margin-top:14px;line-height:1.6;\"><strong>\ud83d\udd25 Fire</strong> = approve for outreach. The next promote window (Mon/Thu 11am ET) drafts and sends the email.<br><strong>Deny</strong> = reject. This business is never emailed, and won't resurface in future runs.</p><p style=\"color:#888;font-size:12px;margin-top:6px;\">All rows inserted as review_status=new in pilot_prospects. Nothing goes out until you Fire it.</p></div>`;\nreturn [{ json: { vertical: col.vertical, location: col.location, total_found: businesses.length,\n  total_emailable: emailable.length, fresh_inserted: prospects.length,\n  duplicates: emailable.length - fresh.length, prospects, digest_html } }];"
      },
      "typeVersion": 2
    },
    {
      "name": "Insert Prospects",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        656,
        336
      ],
      "parameters": {
        "url": "https://YOUR-PROJECT.supabase.co/rest/v1/pilot_prospects",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.prospects) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Prefer",
              "value": "return=minimal"
            }
          ]
        },
        "nodeCredentialType": "supabaseApi"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "name": "Telegram Digest",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1264,
        336
      ],
      "parameters": {
        "url": "https://YOUR-N8N-INSTANCE/webhook/YOUR-WEBHOOK-PATH",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ text: '\\uD83E\\uDDF2 <b>Prospecting run</b>\\n' + $('Map + Dedup').first().json.vertical + ' \u00b7 ' + $('Map + Dedup').first().json.location + '\\nFound ' + $('Map + Dedup').first().json.total_found + ', emailable ' + $('Map + Dedup').first().json.total_emailable + ', NEW inserted ' + $('Map + Dedup').first().json.fresh_inserted + ' (' + $('Map + Dedup').first().json.duplicates + ' dupes skipped)' }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "name": "MX Lookup",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1968,
        -64
      ],
      "parameters": {
        "url": "=https://dns.google/resolve?type=MX&name={{ (($json.verified_email || '@invalid').split('@')[1]) || 'invalid' }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "name": "Apply MX",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        -64
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const biz = $('Extract Email').item.json;\nconst r = $json || {};\nconst hasMX = !!(r.Answer && r.Answer.length) || (r.Status === 0 && Array.isArray(r.Answer) && r.Answer.length > 0);\nreturn { json: { ...biz, mx_record_found: biz.verified_email ? hasMX : false } };"
      },
      "typeVersion": 2
    },
    {
      "name": "Email Digest",
      "type": "n8n-nodes-base.microsoftOutlook",
      "onError": "continueRegularOutput",
      "position": [
        1088,
        336
      ],
      "parameters": {
        "subject": "=New prospects: {{ $('Map + Dedup').first().json.vertical }} - {{ $('Map + Dedup').first().json.location }} ({{ $('Map + Dedup').first().json.fresh_inserted }} new)",
        "bodyContent": "={{ $('Map + Dedup').first().json.digest_html }}",
        "toRecipients": "you@example.com",
        "additionalFields": {
          "from": "you@example.com",
          "bodyContentType": "html"
        }
      },
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "name": "One Digest Email",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        336
      ],
      "parameters": {
        "jsCode": "// Email Digest was firing once per inserted row -> N identical digests.\n// The digest_html (from Map + Dedup) already lists every prospect with Fire/Deny buttons.\n// Send it ONCE; send nothing when no new prospects were inserted.\nreturn $input.all().length ? [$input.first()] : [];"
      },
      "typeVersion": 2
    },
    {
      "name": "Sticky: Overview & Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        -608
      ],
      "parameters": {
        "color": 7,
        "width": 896,
        "height": 320,
        "content": "## Local lead prospecting engine\n\nGive it a business type and a location. It finds local businesses, scrapes their sites for a contact email, validates the email's domain, dedupes against leads you already have, saves the new ones, and emails you one clean digest.\n\n### Setup (replace before running)\n- Credentials: **Google Places API**, **Firecrawl**, **Supabase**, **Microsoft Outlook**, and a **Telegram relay** webhook\n- `YOUR-PROJECT.supabase.co` -> your Supabase URL, with a `pilot_prospects` table\n- Trigger: POST the webhook a body like `{ \"vertical\": \"dentists\", \"location\": \"Orlando FL\" }`\n- Set your notification email and Telegram chat in the digest nodes"
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky: 1 Find businesses",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        -256
      ],
      "parameters": {
        "color": 4,
        "width": 748,
        "height": 358,
        "content": "## 1. Find businesses\nThe **Webhook** receives a business type + location. **Set Config** parses it, **Places Text Search** queries Google Places, and **Map Businesses** turns the results into one item per business."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky: 2 Find a contact email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        -256
      ],
      "parameters": {
        "color": 4,
        "width": 816,
        "height": 358,
        "content": "## 2. Find a contact email\n**Firecrawl** scrapes each business site (and a likely contact page); **Find Contact** and **Extract Email** pull out the best email address."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky: 3 Validate the email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1888,
        -256
      ],
      "parameters": {
        "color": 4,
        "width": 576,
        "height": 358,
        "content": "## 3. Validate the email\n**MX Lookup** checks the domain has real mail servers (a DNS MX record); **Apply MX** keeps only addresses that can actually receive mail. **Collect Results** gathers the businesses with a valid email."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky: 4 Dedup and save",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        144
      ],
      "parameters": {
        "color": 4,
        "width": 560,
        "height": 390,
        "content": "## 4. Dedup and save\n**Fetch Existing** pulls prospects you already have from Supabase, **Map + Dedup** drops anyone already on your list, and **Insert Prospects** saves only the new ones."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky: 5 Send one digest",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        144
      ],
      "parameters": {
        "color": 4,
        "width": 780,
        "height": 390,
        "content": "## 5. Send one digest\n**One Digest Email** bundles the new prospects into a single message (one email, not one per lead). **Email Digest** sends it via Outlook, **Telegram Digest** pings your chat, and **Respond** closes the webhook."
      },
      "typeVersion": 1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Set Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply MX": {
      "main": [
        [
          {
            "node": "Collect Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MX Lookup": {
      "main": [
        [
          {
            "node": "Apply MX",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Config": {
      "main": [
        [
          {
            "node": "Places Text Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map + Dedup": {
      "main": [
        [
          {
            "node": "Insert Prospects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Digest": {
      "main": [
        [
          {
            "node": "Telegram Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Contact": {
      "main": [
        [
          {
            "node": "Firecrawl Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Email": {
      "main": [
        [
          {
            "node": "MX Lookup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Existing": {
      "main": [
        [
          {
            "node": "Map + Dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Businesses": {
      "main": [
        [
          {
            "node": "Firecrawl Scrape",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Results": {
      "main": [
        [
          {
            "node": "Fetch Existing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Digest": {
      "main": [
        [
          {
            "node": "Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Firecrawl Scrape": {
      "main": [
        [
          {
            "node": "Find Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Prospects": {
      "main": [
        [
          {
            "node": "One Digest Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One Digest Email": {
      "main": [
        [
          {
            "node": "Email Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Firecrawl Contact": {
      "main": [
        [
          {
            "node": "Extract Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Places Text Search": {
      "main": [
        [
          {
            "node": "Map Businesses",
            "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 receives a webhook request with a target business vertical and location, finds matching companies via Google Places API, scrapes their websites with Firecrawl to extract and validate contact emails, deduplicates against Supabase, stores new prospects, and sends a…

Source: https://n8n.io/workflows/16424/ — 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 automates bulk email campaigns with built-in validation, deliverability protection, and smart send-time optimization.

HTTP Request, Postgres, Gmail
Marketing & Ads

This workflow is designed to manage the assignment and validation of unique QR code coupons within a lead generation system with SuiteCRM.

HTTP Request, Form Trigger, Google Sheets +1
Marketing & Ads

This workflow acts as an instant SDR that replies to new inbound leads across multiple channels in real time. It first captures and normalizes all incoming lead data into a unified structure. The work

Google Sheets, HTTP Request, Gmail +1
Marketing & Ads

AI Lead Qualification & Roting System. Uses httpRequest, twilio, airtable. Webhook trigger; 26 nodes.

HTTP Request, Twilio, Airtable
Marketing & Ads

A comprehensive n8n workflow template for streamlining influencer application processing with real-time social media data validation, intelligent scoring algorithms, and automated onboarding workflows

N8N Nodes Verifiemail, Stop And Error, HTTP Request +2