Google Maps Leads (with Scalelist backfill)
SkillWeb & browsingThis skill lets your AI build a local-business lead database from Google Maps. You describe the audience and area you are targeting, and it turns the results into a list of prospects with contact details and social profiles attached to each business.
Available today. Use it from your connected AI after setup.
No other account needed.
After adding the skill, tell your AI who you want to reach and where, then ask it to start building your lead list.
Then ask your AI: use the Google Maps Leads (with Scalelist backfill) skill
What your AI can do with it
- Search Google Maps by target audience and geography
- Pull company contacts from each business's own website
- Enrich leads with names, emails, phones, and LinkedIn profiles
- Find Instagram and Facebook profiles for each business
- Score leads using customer reviews
What this skill tells your AI
The instructions your AI receives, as published by apify/awesome-skills in skills/apify-google-maps-leads/SKILL.md and read by ahel’s review.
Build a lead CSV from Google Maps in one pipeline:
- Interview — ask target audience, target geography, and whether to use reviews for lead scoring.
- Scrape — one
compass/crawler-google-placesrun with four add-ons pre-configured (leads enrichment, website contacts, Instagram + Facebook profiles, optional reviews). - Filter & score — apply a review-based score if scoring is on.
- Discover missing names — for places that came back with zero (or unnamed) leads, call
apify/ai-web-scraperon the business website to extract owner/decision-maker names. Scalelist can't work without a name. - Backfill contacts — call
scalelist/phone-finderfor leads with a missing phone,scalelist/email-finderfor leads with a missing email. - Deliver — a deduplicated CSV plus a
run_metadata.jsonsidecar.
Prerequisites
- Apify account (sign up)
- Auth via one of:
apify login(OAuth, if using the Apify CLI)APIFY_TOKENenv var- Token from Apify Console → Settings → Integrations
Either the Apify CLI (recommended for portability) or the Apify MCP connector works. Commands below use the CLI; the MCP path is a drop-in via the call-actor and get-dataset-items tools.
Workflow
Track progress with this checklist:
Task Progress:
- [ ] Step 1: Interview — audience, geography, scoring choice
- [ ] Step 2: Build the Google Maps input
- [ ] Step 3: Run compass/crawler-google-places
- [ ] Step 4: Filter + score (if scoring enabled)
- [ ] Step 5: Discover missing names via apify/ai-web-scraper
- [ ] Step 6: Backfill missing phones and emails via scalelist Actors
- [ ] Step 7: Deduplicate and render the CSV
Step 1: Interview
Ask three questions as one block — don't drip them one by one.
- Target audience — the business type(s) to search for. Free text. Examples:
"dentists","vegan restaurants","boutique hotels","dog groomers, pet stores"(comma-splittable → array). - Target geography — one location per run. City + country reads best (
"Berlin, Germany","Austin, TX"). If the user gives a country only, warn that Maps runs perform best on city-scoped searches. - Use reviews for lead scoring? (y/n) — if yes, we pull reviews (
maxReviews) and filter by review volume + star rating post-run. If no, we skip reviews to cut cost.
Follow-ups only if the user asks for more control:
maxCrawledPlacesPerSearch— default50. Bigger runs = bigger cost.maximumLeadsEnrichmentRecords— default3per place (people to enrich per business). Never0— that disables leads enrichment, which is the point.leadsEnrichmentDepartments— default[](any). Enum values are listed in references/actor-inputs.md.- Minimum star rating pre-filter (
placeMinimumStars) — cheaper than post-filtering when scoring is off. - Review-scoring thresholds — default is
≥ 10 reviews AND ≥ 4.0 rating.
Cost warning threshold. Compute expected_leads = maxCrawledPlacesPerSearch × maximumLeadsEnrichmentRecords. If it exceeds 200, restate the number back to the user and confirm before running. Leads enrichment is the dominant cost line; a slip here is what surprises people.
Step 2: Build the Google Maps input
Set every field below on every run. Full field reference in references/actor-inputs.md.
| Field | Value |
|---|---|
searchStringsArray | audience as array (e.g. ["dentists"]) |
locationQuery | geography free text |
maxCrawledPlacesPerSearch | user override or default 50 |
language | "en" unless user specifies |
scrapePlaceDetailPage | true — needed for phone + hours + address |
skipClosedPlaces | true — permanent/temporary closures are dead leads |
scrapeContacts | true — Add-on: Company contacts enrichment (from website) ($) |
scrapeSocialMediaProfiles | {"instagrams": true, "facebooks": true, "youtubes": false, "tiktoks": false, "twitters": false} — Instagram + Facebook profile enrichment |
maximumLeadsEnrichmentRecords | user override or default 3 — Add-on: Business leads enrichment ($) |
leadsEnrichmentDepartments | user override or [] |
verifyLeadsEnrichmentEmails | true — always, never false |
maxReviews | 10 if scoring is on, 0 otherwise |
reviewsSort | "newest" if scoring is on |
scrapeSocialMediaProfiles auto-enables scrapeContacts. Both are billed on top of the base scrape — see the Actor's pricing tab.
Step 3: Run compass/crawler-google-places
apify actors call "compass/crawler-google-places" \
--input '<JSON_FROM_STEP_2>' \
--user-agent apify-awesome-skills/apify-google-maps-leads \
--json 2>/dev/null
Capture id (runId) and defaultDatasetId. Pull the dataset:
apify datasets get-items <DATASET_ID> --format json \
--user-agent apify-awesome-skills/apify-google-maps-leads 2>/dev/null > places.json
Leads enrichment adds 30–90 s per place — expect long runs. If a run times out, the dataset already holds partial results; pull by datasetId.
Step 4: Filter and score
Applied to the raw places.json in order. Full logic in references/scoring-and-backfill.md.
- Spurious-match filter (always on). Drop leads whose
leadsEnrichment[].companyWebsitehostname doesn't match theplace.websitehostname. Same failure mode asapify-verified-email-finder— a global-fallback lead attributed to unrelated places by substring. Count drops inrun_metadata.json. - Review-based score (only if scoring is on).
- Default keep-logic:
place.reviewsCount >= 10 AND place.totalScore >= 4.0. - Emit a numeric
Lead Scorecolumn:round(place.totalScore * log10(place.reviewsCount + 1), 2). Higher = better local reputation. - If scoring is off,
Lead Scoreis blank.
- Default keep-logic:
- Empty-lead surfacing. If a place has zero enriched leads, keep one row for the place with blank person fields — the user sees the business but knows nobody was found. Never silently drop.
Step 5: Discover missing names via apify/ai-web-scraper
Scalelist needs a person name (or LinkedIn URL) to look anything up. When a place has zero enriched leads — or leads with blank firstName / lastName — but has a working website, run apify/ai-web-scraper on that website to extract owner/decision-maker names.
When to run this step per place (all conditions must hold):
place.websiteis non-empty (nothing to scrape otherwise)- Either
leadsEnrichment[]is empty, or every lead has a blankfirstName - The place survived Step 4's scoring filter (don't spend on places we're going to drop)
Payload — same input pattern as the Apify AI Web Scraper "list of writers" example, retargeted from blog authors to local-business decision-makers:
{
"startUrls": [{"url": "<place.website>"}],
"extractionMode": "agentic",
"prompt": "Find the owner, founder, or key decision-makers of this business. For each person, include their full name and job title. Prioritize pages like /about, /team, /contact, or the site footer.",
"maxPagesToVisit": 20,
"maxCrawlDepth": 3
}
Defaults trimmed vs. the blog example (which uses maxPagesToVisit: 100 / maxCrawlDepth: 5) — small-business sites are typically shallow, and this is one call per website.
apify actors call "apify/ai-web-scraper" \
--input '{"startUrls":[{"url":"<place.website>"}],"extractionMode":"agentic","prompt":"...","maxPagesToVisit":20,"maxCrawlDepth":3}' \
--user-agent apify-awesome-skills/apify-google-maps-leads \
--json 2>/dev/null
One place per call — the Actor's crawl fans out from startUrls, so batching multiple business sites in one startUrls array would mix results. Run one call per place website. Parallelize across places if you have many.
Merge results back onto the place:
- The Actor returns rows with
url,data,markdown.dataholds the extracted people — expect{ "people": [{ "name": "...", "jobTitle": "..." }] }or an array of such objects (LLM output shape varies). - For each extracted person, split
nameon the last space intofirstName/lastName. - Append a new lead into
place.leadsEnrichment[]withfirstName,lastName,jobTitle,companyWebsite = place.website, and markBackfill Source = "ai-web-scraper". - Cap at 3 new leads per place — the Actor sometimes returns lots of tangential names (past employees, testimonial subjects).
Skip conditions:
- Skip entirely if the user opts out of name discovery up front (offer this as a follow-up when running large batches — this is the priciest step per place).
- Skip if
place.websitereturns a redirect to a social profile (Facebook page, Instagram) — the AI scraper handles JS sites but Meta login walls will burn budget for nothing.
Step 6: Backfill missing contacts
For every lead surviving Step 5, check what's missing.
Phone backfill. Collect leads with a non-blank name and a blank phone. Group into batches of 100. Payload for scalelist/phone-finder:
{
"leads": [
{"first_name": "...", "last_name": "...", "company_domain": "...", "linkedin_profile_url": "..."}
]
}
linkedin_profile_url alone is sufficient; otherwise supply first_name + last_name + company_domain (preferred) or company_name.
apify actors call "scalelist/phone-finder" \
--input '{"leads":[...]}' \
--user-agent apify-awesome-skills/apify-google-maps-leads \
--json 2>/dev/null
Email backfill. Collect leads with a non-blank name and a blank email. Payload for scalelist/email-finder:
{
"leads": [
{"first_name": "...", "last_name": "...", "company_domain": "...", "company_name": "..."}
]
}
first_name + last_name are required; company_domain beats company_name for match rate.
apify actors call "scalelist/email-finder" \
--input '{"leads":[...]}' \
--user-agent apify-awesome-skills/apify-google-maps-leads \
--json 2>/dev/null
Both Actors are pay-per-event: you're only charged for successful matches. Merge the returned phones/emails back onto the original leads by lowercased first_name + last_name + company_domain.
Skip conditions. Don't call scalelist if the lead has no first+last name AND no LinkedIn URL — nothing to look up. Don't call it if the user says "skip backfill" up front (surface this as an offer if the initial maximumLeadsEnrichmentRecords was high, since the cost stacks).
Step 7: Deduplicate and render the CSV
Full column schema in references/output-format.md. Twenty columns including Lead Score, Instagram Followers, Facebook Followers, Backfill Source.
- Dedupe by lowercased
emailwhere present; otherwise by lowercasedfirst_name + last_name + place_id. - Sort by
Lead Scoredescending when scoring is on; otherwise byBusinessalphabetically. - Deliverable header: state whether scoring was on, the review thresholds, and the spurious-match drop count. Offer to re-render without scoring if the kept-row count is low.
- Write a
run_metadata.jsonsidecar next to the CSV withrunId,datasetId, and stats (placesScraped,rawLeads,spuriousDropped,phonesBackfilled,emailsBackfilled,keptRows).
Worked example
See examples/example-dentists-berlin.md — full inputs + sample CSV rows for a "dentists in Berlin, Germany, scoring on" run.
Quality rules (always enforce)
- Guard rails:
verifyLeadsEnrichmentEmails: trueandskipClosedPlaces: trueon every run. - Provenance: every row carries
Source Query,Business, andPlace ID.run_metadata.jsoncarries the ApifyrunId+datasetId. - No fabrication: missing fields stay blank. Never invent an email or phone.
- Cost transparency: if
expected_leads > 200, restate and confirm before running Step 3. - Scoring is optional. If the user said no to scoring, keep the
Lead Scorecolumn but leave it blank — don't invent one.
Troubleshooting
Run TIMED-OUT— LowermaxCrawledPlacesPerSearchormaximumLeadsEnrichmentRecords. Enrichment is the slow part.- All leads dropped by spurious-match — The enrichment service returned only global-fallback leads. Real fix: none. Surface the count.
- Zero backfilled phones/emails — Scalelist needs a person name and a company domain (or LinkedIn URL). If leads came back without domains, backfill has nothing to work with. Check
place.websitewas populated. If names are also missing, Step 5 (ai-web-scraper) should have populated them — check its output. apify/ai-web-scraperreturned zero people — Site is a single-page landing (no /about /team /contact), a JS-app that renders after the crawl budget, or a redirect to Facebook/Instagram. No fix; accept the miss and let scalelist skip that place.- Instagram / Facebook fields blank — The place's website didn't link to those profiles, so nothing to enrich. Not an error.
Actor not found: scalelist/phone-finder— The Actor is on the Apify Store but not pre-approved on your account. Open it once in the console to accept the terms.- Cost surprise — Pull the breakdown from the console. Usual culprits:
maxReviews > 10combined with highmaxCrawledPlacesPerSearch, or forgetting to disable YouTube/TikTok/X social enrichment (they cost the same as IG/FB).
For error recovery patterns shared across Apify skills, see references/gotchas.md.
Signals
- GitHub stars
- 252
- Forks
- 65
- Last commit
- Sep 2026
ahel review
S4info
community integration — published by apify, not google
Automated review, not a security audit. Ruleset v1.
Advanced
- Catalog kind
- skill
- Gateway key
apify-google-maps-leads- Source
- github.com/apify/awesome-skills