Google Maps to Google Sheets, clean leads with emails
Copy-pasting Google Maps listings into a spreadsheet sounds easy. Then you do it for an hour, realize half the “websites” are junk, and you still don’t have a single usable email.
This Maps lead automation hits agency owners and freelancers hard, because lead volume is tied to billable time. A sales lead inside a small business feels it too, because bad data means bad outreach and wasted follow-ups.
This workflow pulls local businesses from Google Maps, cleans the websites, extracts real emails, and drops a deduped lead list into Google Sheets. You’ll see what it does, what you need, and how to get it running without turning into a scraper detective.
How This Automation Works
Here’s the complete workflow you’ll be setting up:
n8n Workflow Template: Google Maps to Google Sheets, clean leads with emails
flowchart LR
subgraph sg0["When clicking ‘Test workflow’ Flow"]
direction LR
n0@{ icon: "mdi:play-circle", form: "rounded", label: "When clicking ‘Test workflow’", pos: "b", h: 48 }
n1@{ icon: "mdi:robot", form: "rounded", label: "Basic LLM Chain", pos: "b", h: 48 }
n2@{ icon: "mdi:robot", form: "rounded", label: "Structured Output Parser", pos: "b", h: 48 }
n3@{ icon: "mdi:brain", form: "rounded", label: "OpenAI Chat Model", pos: "b", h: 48 }
n4@{ icon: "mdi:swap-vertical", form: "rounded", label: "Split Out", pos: "b", h: 48 }
n5@{ icon: "mdi:swap-vertical", form: "rounded", label: "Edit Fields", pos: "b", h: 48 }
n6@{ icon: "mdi:swap-vertical", form: "rounded", label: "Loop Over Items", pos: "b", h: 48 }
n7@{ icon: "mdi:swap-vertical", form: "rounded", label: "Edit Fields3", pos: "b", h: 48 }
n8@{ icon: "mdi:cog", form: "rounded", label: "Limit", pos: "b", h: 48 }
n9@{ icon: "mdi:cog", form: "rounded", label: "Wait", pos: "b", h: 48 }
n10@{ icon: "mdi:cog", form: "rounded", label: "Wait1", pos: "b", h: 48 }
n11["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/code.svg' width='40' height='40' /></div><br/>Extract Emails"]
n12["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/code.svg' width='40' height='40' /></div><br/>Extract URLs"]
n13["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/httprequest.dark.svg' width='40' height='40' /></div><br/>Scrape Site"]
n14@{ icon: "mdi:swap-horizontal", form: "rounded", label: "Filter Out Empties", pos: "b", h: 48 }
n15@{ icon: "mdi:swap-horizontal", form: "rounded", label: "Filter Google URLs", pos: "b", h: 48 }
n16@{ icon: "mdi:cog", form: "rounded", label: "Remove Duplicates (2)", pos: "b", h: 48 }
n17@{ icon: "mdi:cog", form: "rounded", label: "Remove Duplicates2", pos: "b", h: 48 }
n18@{ icon: "mdi:swap-vertical", form: "rounded", label: "Loop Over Items3", pos: "b", h: 48 }
n19@{ icon: "mdi:cog", form: "rounded", label: "Limit1", pos: "b", h: 48 }
n20@{ icon: "mdi:swap-vertical", form: "rounded", label: "Split Out2", pos: "b", h: 48 }
n21@{ icon: "mdi:swap-vertical", form: "rounded", label: "Edit Fields1", pos: "b", h: 48 }
n22["<div style='background:#f5f5f5;padding:10px;border-radius:8px;display:inline-block;border:1px solid #e0e0e0'><img src='https://flowpast.com/wp-content/uploads/n8n-workflow-icons/httprequest.dark.svg' width='40' height='40' /></div><br/>Scrape Google Maps"]
n9 --> n11
n8 --> n6
n10 --> n14
n19 --> n18
n4 --> n5
n20 --> n16
n5 --> n8
n13 --> n9
n21 --> n1
n7 --> n6
n12 --> n15
n11 --> n18
n1 --> n4
n6 --> n22
n18 --> n10
n18 --> n13
n3 -.-> n1
n15 --> n17
n14 --> n20
n17 --> n19
n22 --> n12
n16 --> n7
n2 -.-> n1
n0 --> n21
end
%% Styling
classDef trigger fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
classDef ai fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef aiModel fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px
classDef decision fill:#fff8e1,stroke:#f9a825,stroke-width:2px
classDef database fill:#fce4ec,stroke:#c2185b,stroke-width:2px
classDef api fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef code fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef disabled stroke-dasharray: 5 5,opacity: 0.5
class n0 trigger
class n1,n2 ai
class n3 aiModel
class n14,n15 decision
class n13,n22 api
class n11,n12 code
classDef customIcon fill:none,stroke:none
class n11,n12,n13,n22 customIcon
Why This Matters: Clean local leads are painfully manual
Local lead gen breaks down in the boring middle. You start with a niche and a city, search Google Maps, open listings, copy names, paste URLs, and then discover the website field points to a Google redirect, a Facebook page, or nothing at all. Next comes the “find an email” part: clicking around, digging through footers, and guessing which contact form might convert. By the time you’re done, your sheet looks full, but outreach-ready leads are rare. And if you do this weekly, the mental load is worse than the time cost.
It adds up fast. Here’s where it breaks down in real life:
- You spend about 5–10 minutes per business just to get a usable website and still miss obvious duplicates.
- Google-owned and directory links sneak into your sheet, so you waste outreach on “leads” that aren’t leads.
- Email hunting becomes a separate project, and it’s easy to copy the wrong address or grab a dead inbox.
- As soon as you try to scale to multiple neighborhoods, you hit captchas, rate limits, and inconsistent results.
What You’ll Build: Google Maps leads to a clean Google Sheet
This n8n workflow turns “business type + city” into a shareable lead list that’s actually ready for outreach. You start by entering your target (for example, plumbers in Los Angeles). OpenAI then generates hyperlocal search queries so you cover neighborhoods, not just the obvious city-wide term. Next, the workflow requests Google Maps results, pulls business details and website links, and removes junk results before they pollute your spreadsheet. After that, it visits each website, downloads the homepage HTML, and extracts emails using a simple regex approach (fast, surprisingly effective). Finally, it deduplicates both websites and emails so your sheet stays clean when you run it again next week.
The flow begins with your inputs, then expands them into a list of targeted searches. It scrapes Maps results through Zyte for reliability, batches requests with small waits so you don’t overwhelm anything, and finishes by outputting validated-looking contact fields into Google Sheets.
What You’re Building
| What Gets Automated | What You’ll Achieve |
|---|---|
|
|
Expected Results
Say you want 100 local leads for one niche in one city. Manually, assume about 6 minutes per lead to copy details, verify the website, and hunt an email. That’s roughly 10 hours of distraction-heavy work. With this workflow, you spend about 10 minutes setting the business type and city, then let it run while batching requests and visiting sites, which usually lands you in the 30–45 minute range depending on volume. You still review the sheet, but you’re reviewing, not collecting.
Before You Start
- n8n instance (try n8n Cloud free)
- Self-hosting option if you prefer (Hostinger works well)
- OpenAI for generating hyperlocal search queries.
- Zyte API to scrape Google Maps reliably.
- OpenAI API key + Zyte API key (get both from your OpenAI and Zyte dashboards)
Skill level: Intermediate. You won’t write code from scratch, but you should feel comfortable adding credentials, testing nodes, and reading a Google Sheet output.
Want someone to build this for you? Talk to an automation expert (free 15-minute consultation).
Step by Step
You start the workflow with a city and business type. In n8n, that input is set in a dedicated “Set City & Business” step (or you can later swap it to a webhook if you want to trigger it from a form).
AI expands your search coverage. OpenAI generates a list of neighborhood-style queries, then the workflow splits that list into individual searches so you can pull more complete Maps coverage without thinking up 30 variations yourself.
Maps results get scraped and cleaned. Each query runs through an HTTP request (via Zyte), results are batched with short waits, and the workflow removes empty items, Google-owned links, and duplicates before doing anything else.
Websites are visited to extract emails. The workflow fetches homepage HTML, pulls likely emails with a regex-based parser, and then deduplicates again so multiple pages don’t produce the same contact over and over.
You can easily modify the search prompt to target zip codes instead of neighborhoods based on your needs. See the full implementation guide below for customization options.
Step-by-Step Implementation Guide
Step 1: Configure the Manual Trigger
Start the workflow manually so you can control the city and business inputs before generating search queries.
- Add Manual Start Trigger as the workflow trigger.
- Connect Manual Start Trigger to Set City & Business.
- In Set City & Business, set City to
Californiaand Business toMarketing Agencies.
Step 2: Connect OpenAI
The workflow uses an OpenAI model to generate localized Google Maps queries.
- Open OpenAI Chat Engine.
- Credential Required: Connect your openAiApi credentials.
- Confirm the model is set to
gpt-4o-mini.
Step 3: Set Up the AI Query Generator
Generate a structured list of neighborhood queries based on the city and business input.
- Open LLM Query Builder and confirm Prompt Type is set to
define. - Keep the prompt text as provided and ensure it references
{{ $json.City }}and{{ $json.Business }}. - Ensure Structured Result Parser is connected as the output parser with Input Schema set to
{ "output": ["plumbers+in+chinatown","plumbers+in+westlake"] }. - Connect LLM Query Builder to Expand Location List.
Step 4: Configure Google Maps Query Execution
Prepare query records, limit volume, and request Google Maps search results.
- In Expand Location List, set Field to Split Out to
output.locations. - In Map Query Fields, set output to
{{ $json['output.locations'] }}and location to{{ $('Set City & Business').item.json.City }}. - In Cap Query Count, set Max Items to
5. - Connect Cap Query Count to Batch Through Queries.
- In Request Maps Results, set URL to
https://www.google.com/maps/search/{{ $json.output }}. - From Batch Through Queries, route output to Request Maps Results.
Step 5: Parse Websites and Extract Emails
Extract website URLs from Maps results, visit each site, and parse email addresses.
- In Parse Website Links, keep the JavaScript that extracts URLs using the regex in
jsCode. - In Exclude Google Links, verify filters exclude
schema,google,gg, andgstaticfrom{{ $json.website }}. - Connect Exclude Google Links to Deduplicate Websites, then to Cap Website Count with Max Items set to
20. - In Batch Site Visits, ensure it receives items from Cap Website Count and note that Batch Site Visits outputs to both Pause Before Filtering and Fetch Website Content in parallel.
- In Fetch Website Content, set URL to
{{ $json.website }}and keep redirects disabled. - In Delay Between Requests, set Amount to
1to throttle site requests. - In Parse Email Addresses, keep the JavaScript that extracts emails from
$input.first().json.data. - In Remove Empty Results, keep the condition
{{ $json.emails }}exists, then connect to Expand Email List. - In Expand Email List, set Field to Split Out to
emails, then connect to Deduplicate Emails and Assemble Contact Fields. - In Assemble Contact Fields, set email to
{{ $json.emails }}, search term to{{ $('Batch Through Queries').item.json.output }}, and location to{{ $('Batch Through Queries').item.json.location }}.
data, and consider increasing Delay Between Requests.Step 6: Test and Activate Your Workflow
Run a manual test to verify query generation, website extraction, and email parsing.
- Click Execute Workflow while Manual Start Trigger is selected.
- Confirm that LLM Query Builder outputs a structured list to Expand Location List.
- Verify Request Maps Results returns HTML and Parse Website Links extracts URLs.
- Check that Parse Email Addresses returns an
emailsarray and Assemble Contact Fields outputs email, search term, and location. - Once successful, toggle the workflow Active for production use.
Troubleshooting Tips
- OpenAI credentials can expire or be restricted by project settings. If things break, check your OpenAI API key status and usage limits in the OpenAI dashboard first.
- If you’re using Wait nodes or external rendering, processing times vary. Bump up the wait duration if downstream nodes fail on empty responses.
- Zyte requests can fail if your API key is wrong or your plan is out of credits. Check the Zyte dashboard for recent request errors and quota before you assume the workflow logic is wrong.
Quick Answers
About 30 minutes if you already have your API keys.
No. You’ll mostly connect credentials and adjust a few Set fields. The email parsing uses a built-in code node, but you can keep it as-is.
Yes. n8n has a free self-hosted option and a free trial on n8n Cloud. Cloud plans start at $20/month for higher volume. You’ll also need to factor in OpenAI and Zyte API costs, which usually come out to a few dollars per batch for small runs.
Two options: n8n Cloud (managed, easiest setup) or self-hosting on a VPS. For self-hosting, Hostinger VPS is affordable and handles n8n well. Self-hosting gives you unlimited executions but requires basic server management.
Yes, and you should. The most common change is swapping the OpenAI prompt in the “LLM Query Builder” to use “service + zip code” or “service + suburb” formats, depending on how your market searches. You can also tighten filtering by adjusting the “Exclude Google Links” logic, and update the regex in “Parse Email Addresses” if you want to reject free inboxes or only keep role-based emails like info@ and sales@.
Usually it’s an invalid API key or you ran out of Zyte credits. Update the Zyte credential used by the HTTP Request nodes, then rerun a single query to confirm. If it works for one query but fails in batches, you may also be hitting rate limits, so increase the “Delay Between Requests” wait time.
If you self-host n8n, there’s no execution limit and capacity mostly depends on your server and your Zyte/OpenAI limits. On n8n Cloud, the practical cap is your monthly executions on the plan, and this workflow can use a lot of executions if you scrape many queries and visit many sites. In day-to-day use, teams often run this in batches of a few dozen to a couple hundred businesses per niche so it stays stable and easy to review.
For this kind of scraping-and-cleaning workflow, n8n is usually the better fit. You need batching, waits, deduplication, and a bit of custom parsing, and those steps get expensive or awkward in tools that price per task. n8n also gives you the self-hosting option, which matters when you’re pulling leads regularly. Zapier or Make can still work if you only want to move already-clean data from one app to another. If you’re on the fence, Talk to an automation expert and we’ll map the cheapest route.
Once this is running, local lead gen stops being a weekly grind. You get cleaner data, faster, and your outreach time finally goes toward conversations instead of copy-paste.
Need Help Setting This Up?
Our automation experts can build and customize this workflow for your specific needs. Free 15-minute consultation—no commitment required.