Cold Email Local Business
gmapsscraper/google-maps-agent-skills
Write hyper-personalized cold emails to local businesses using their Google Maps data (CSV).
Agent skill
by growthenginenowoslawski in growthenginenowoslawski/coldoutboundskills
Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment.
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builder --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/google-maps-list-builder .claude/skills/google-maps-list-builder && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "google-maps-list-builder" agent skill from https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builder into .claude/skills/google-maps-list-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "google-maps-list-builder", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builderType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builder --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/google-maps-list-builder .agents/skills/google-maps-list-builder && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "google-maps-list-builder" agent skill from https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builder into .agents/skills/google-maps-list-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "google-maps-list-builder", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builder --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/google-maps-list-builder .cursor/skills/google-maps-list-builder && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "google-maps-list-builder" agent skill from https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builder into .cursor/skills/google-maps-list-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "google-maps-list-builder", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/growthenginenowoslawski/coldoutboundskills.git --path skills/google-maps-list-builder--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builder --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/google-maps-list-builder .gemini/skills/google-maps-list-builder && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "google-maps-list-builder" agent skill from https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builder into .gemini/skills/google-maps-list-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "google-maps-list-builder", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builderInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/google-maps-list-builder .github/skills/google-maps-list-builder && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "google-maps-list-builder" agent skill from https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builder into .github/skills/google-maps-list-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "google-maps-list-builder", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builder --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/google-maps-list-builder .opencode/skills/google-maps-list-builder && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "google-maps-list-builder" agent skill from https://github.com/growthenginenowoslawski/coldoutboundskills/tree/main/skills/google-maps-list-builder into .opencode/skills/google-maps-list-builder/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "google-maps-list-builder", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
google-maps-list-builderScrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment.
Google Maps List Builder is an agent skill from growthenginenowoslawski/coldoutboundskills. Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment. Best for SMB campaigns targeting restaurants, clinics, gyms, salons, contractors, etc. Uses RapidAPI Maps Data API. Output feeds directly into /blitz-list-builder (to find owner contacts) or //list-builder Phase 5 (if you have names already).
Its SKILL.md is about 6.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Sales & Support, covering Cold outreach, Web scraping and CSV and tabular files. It works with Google Maps Platform. The repository describes itself as: Open-source Claude Code skills for cold email and outbound sales. Grade campaigns, export Prospeo searches, scrape Google Maps — all from Claude Code. The licence is MIT.
5 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 25c5d85. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
npmnpxflyFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
rapidapi.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
RAPIDAPI_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Google Maps List Builder loads about 6.2k tokens when it runs. Until then it costs about 94 tokens; SKILL.md has 920 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
Or create a `.env` file (add `.env` to `.gitignore`):Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from growthenginenowoslawski/coldoutboundskills at commit 25c5d85, republished under its MIT licence (© growthenginenowoslawski). 920 words, ~6,160 tokens.
.claude/skills/google-maps-list-builder/SKILL.md (or your agent's skills folder).A self-contained tool for scraping business listings from Google Maps. Give it a search query (e.g., "pizza restaurant") and a location (zip code, city, or coordinates), and it returns structured data for every matching business — written to CSV.
Google Maps gives you COMPANIES (name, domain, phone, address, ratings). It does NOT give you PEOPLE. To run cold email:
company_domain/icp-prompt-builder on a sample of 50 — tune a qualification prompt to filter out bad fits before paying for downstream enrichment/blitz-list-builder with the filtered CSV → adds owners/managers to each business/list-builder (Phase 5, emails) → fills in missing emails/cold-email-starter-kit's smartlead-add-leads.ts → upload to SmartleadThis skill is only the first step.
This is a required step. Do not skip it.
Google Maps will happily return 10,000 "pizza restaurants in Illinois," but most of those won't match your actual ICP (maybe you only want 50-200 seat operators, or only ones without online ordering). Before spending on enrichment, sample ~50 results and run /icp-prompt-builder:
Why required: downstream owner-finding (via /blitz-list-builder) and email waterfall cost $0.10-$0.30 per contact. On a 10,000-business scrape, that's $1K-$3K. Qualifying upfront saves 50-80% of that spend on average.
X-RapidAPI-Key header on any endpoint page)That's it. No Google Cloud account, no OAuth, no billing setup beyond RapidAPI.
Create a new project directory and initialize it:
mkdir google-maps-scraper && cd google-maps-scraper
npm init -y
npm install typescript bottleneck
npm install -D @types/node tsxAdd to package.json scripts:
{
"scripts": {
"scrape": "tsx src/index.ts"
}
}Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src",
"skipLibCheck": true
},
"include": ["src"]
}Set your API key as an environment variable:
export RAPIDAPI_KEY=your_key_hereOr create a .env file (add .env to .gitignore):
RAPIDAPI_KEY=your_key_heregoogle-maps-scraper/
data/
us-zip-codes.csv # 42,734 US zip codes with city, state, lat/lng, population
src/
index.ts # CLI entry point
client.ts # RapidAPI Maps Data client with rate limiting
types.ts # TypeScript interfaces
csv.ts # CSV export
zips.ts # Zip code loader (filter by state, city, population)The repo includes data/us-zip-codes.csv — a complete US zip code reference with 42,734 entries. Columns:
zip,primary_city,state,timezone,area_codes,world_region,country,latitude,longitude,irs_estimated_populationThis lets you scrape an entire state or metro area without manually listing zip codes. The src/zips.ts loader provides filtering by state, city, and minimum population.
export interface SearchParams {
query: string; // "pizza restaurant", "dentist", "gym"
lat?: number; // Center latitude (optional if using "query in zipcode" format)
lng?: number; // Center longitude
limit?: number; // Max results per search (default 20, max 20)
zoom?: number; // Map zoom level (default 13 = neighborhood)
country?: string; // Country code (default "us")
}
export interface Place {
place_id: string;
name: string;
address: string;
lat: number;
lng: number;
rating?: number;
reviews_count?: number;
phone?: string;
website?: string;
types?: string[];
category?: string;
}
export interface ScrapeResult {
query: string;
location: string;
total_results: number;
unique_results: number;
places: Place[];
duration_ms: number;
}Loads and filters the bundled zip code CSV. Lets you target by state, city name, or minimum population.
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
export interface ZipEntry {
zip: string;
city: string;
state: string;
lat: number;
lng: number;
population: number;
}
let cache: ZipEntry[] | null = null;
function loadAll(): ZipEntry[] {
if (cache) return cache;
const __dirname = dirname(fileURLToPath(import.meta.url));
const csvPath = join(__dirname, '..', 'data', 'us-zip-codes.csv');
const raw = readFileSync(csvPath, 'utf-8');
const lines = raw.trim().split('\n').slice(1); // skip header
cache = lines.map(line => {
// Handle quoted fields (area_codes can contain commas)
const parts: string[] = [];
let current = '';
let inQuotes = false;
for (const ch of line) {
if (ch === '"') { inQuotes = !inQuotes; continue; }
if (ch === ',' && !inQuotes) { parts.push(current); current = ''; continue; }
current += ch;
}
parts.push(current);
return {
zip: parts[0]?.padStart(5, '0') || '',
city: parts[1] || '',
state: parts[2] || '',
lat: parseFloat(parts[7]) || 0,
lng: parseFloat(parts[8]) || 0,
population: parseInt(parts[9]) || 0,
};
}).filter(z => z.zip.length === 5);
return cache;
}
/** Get zips for a US state (2-letter code, e.g. "CA", "TX") */
export function getZipsByState(stateCode: string): ZipEntry[] {
return loadAll().filter(z => z.state.toUpperCase() === stateCode.toUpperCase());
}
/** Get zips for a city name (case-insensitive, partial match) */
export function getZipsByCity(city: string, state?: string): ZipEntry[] {
const cityLower = city.toLowerCase();
return loadAll().filter(z => {
const cityMatch = z.city.toLowerCase().includes(cityLower);
const stateMatch = !state || z.state.toUpperCase() === state.toUpperCase();
return cityMatch && stateMatch;
});
}
/** Get zips with population above a threshold */
export function getZipsByMinPopulation(minPop: number, state?: string): ZipEntry[] {
return loadAll().filter(z => {
const popMatch = z.population >= minPop;
const stateMatch = !state || z.state.toUpperCase() === state.toUpperCase();
return popMatch && stateMatch;
});
}
/** Get all loaded zip entries */
export function getAllZips(): ZipEntry[] {
return loadAll();
}This is the core API client. It handles rate limiting (2 req/sec) and retries with exponential backoff.
import Bottleneck from 'bottleneck';
import type { SearchParams, Place } from './types.js';
interface RawSearchResponse {
data?: Array<{
place_id?: string;
title?: string;
name?: string;
address?: string;
latitude?: number;
longitude?: number;
rating?: number;
reviews?: number;
phone?: string;
website?: string;
types?: string[];
type?: string;
category?: string;
}>;
error?: string;
}
interface GeocodingResponse {
latitude?: number;
longitude?: number;
formatted_address?: string;
error?: string;
}
export class GoogleMapsClient {
private limiter: Bottleneck;
private apiKey: string;
private host = 'maps-data.p.rapidapi.com';
private maxRetries: number;
constructor(opts: { apiKey: string; requestsPerSecond?: number; maxRetries?: number }) {
this.apiKey = opts.apiKey;
this.maxRetries = opts.maxRetries ?? 3;
this.limiter = new Bottleneck({
maxConcurrent: 1,
minTime: Math.floor(1000 / (opts.requestsPerSecond ?? 2)),
});
}
/** Search Google Maps for businesses */
async search(params: SearchParams): Promise<Place[]> {
const response = await this.request<RawSearchResponse>('searchmaps.php', {
query: params.query,
limit: String(params.limit ?? 20),
country: params.country ?? 'us',
...(params.lat != null && { lat: String(params.lat) }),
...(params.lng != null && { lng: String(params.lng) }),
...(params.zoom != null && { zoom: String(params.zoom) }),
});
if (response.error) throw new Error(`Search failed: ${response.error}`);
return this.transform(response.data || []);
}
/** Geocode a zip code or address to lat/lng */
async geocode(query: string, country = 'us'): Promise<{ lat: number; lng: number }> {
const response = await this.request<GeocodingResponse>('geocoding.php', {
query: `${query}, ${country.toUpperCase()}`,
});
if (!response.latitude || !response.longitude) {
throw new Error(`Could not geocode: ${query}`);
}
return { lat: response.latitude, lng: response.longitude };
}
private async request<T>(endpoint: string, params: Record<string, string>): Promise<T> {
return this.limiter.schedule(() => this.requestWithRetry<T>(endpoint, params));
}
private async requestWithRetry<T>(
endpoint: string,
params: Record<string, string>,
attempt = 0
): Promise<T> {
const url = new URL(`https://${this.host}/${endpoint}`);
for (const [k, v] of Object.entries(params)) {
if (v != null) url.searchParams.set(k, v);
}
try {
const res = await fetch(url.toString(), {
headers: {
'X-RapidAPI-Key': this.apiKey,
'X-RapidAPI-Host': this.host,
},
});
if (!res.ok) {
const err: any = new Error(`API ${res.status}: ${res.statusText}`);
err.statusCode = res.status;
throw err;
}
return (await res.json()) as T;
} catch (err: any) {
const retryable =
attempt < this.maxRetries &&
(err.statusCode === 429 || err.statusCode >= 500 ||
err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT');
if (retryable) {
const delay = 1000 * Math.pow(2, attempt);
console.log(` Retry ${attempt + 1}/${this.maxRetries} in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
return this.requestWithRetry<T>(endpoint, params, attempt + 1);
}
throw err;
}
}
private transform(data: NonNullable<RawSearchResponse['data']>): Place[] {
return data.map(item => ({
place_id: item.place_id || '',
name: item.title || item.name || '',
address: item.address || '',
lat: item.latitude || 0,
lng: item.longitude || 0,
rating: item.rating,
reviews_count: item.reviews,
phone: item.phone,
website: item.website,
types: item.types || (item.type ? [item.type] : []),
category: item.category || item.type,
}));
}
}import { writeFile, mkdir } from 'fs/promises';
import { dirname } from 'path';
import type { Place } from './types.js';
const HEADERS = [
'place_id', 'name', 'address', 'phone', 'website',
'rating', 'reviews_count', 'lat', 'lng', 'category',
];
function escape(val: string | number | undefined | null): string {
if (val == null) return '';
const s = String(val);
return s.includes(',') || s.includes('"') || s.includes('\n')
? `"${s.replace(/"/g, '""')}"`
: s;
}
export async function exportCSV(places: Place[], outputPath: string): Promise<void> {
await mkdir(dirname(outputPath), { recursive: true });
const lines = [
HEADERS.join(','),
...places.map(p =>
HEADERS.map(h => escape(p[h as keyof Place])).join(',')
),
];
await writeFile(outputPath, lines.join('\n') + '\n', 'utf-8');
}import { GoogleMapsClient } from './client.js';
import { exportCSV } from './csv.js';
import { getZipsByState, getZipsByCity, getZipsByMinPopulation } from './zips.js';
import type { Place } from './types.js';
function dedup(places: Place[]): Place[] {
const seen = new Set<string>();
return places.filter(p => {
if (seen.has(p.place_id)) return false;
seen.add(p.place_id);
return true;
});
}
function getArg(args: string[], prefix: string): string | undefined {
const match = args.find(a => a.startsWith(prefix));
return match ? match.split('=').slice(1).join('=') : undefined;
}
async function main() {
const args = process.argv.slice(2);
const query = getArg(args, '--query=');
const zips = getArg(args, '--zips=');
const cities = getArg(args, '--cities=');
const state = getArg(args, '--state=');
const minPop = getArg(args, '--min-pop=');
const limit = parseInt(getArg(args, '--limit=') || '20', 10);
const output = getArg(args, '--output=') || './output/results.csv';
if (!query || (!zips && !cities && !state)) {
console.log(`
Google Maps Scraper
USAGE:
npm run scrape -- --query="pizza restaurant" --zips=10014,10013,10012
npm run scrape -- --query="dentist" --state=TX
npm run scrape -- --query="dentist" --state=TX --min-pop=10000
npm run scrape -- --query="gym" --cities="Austin TX,Dallas TX"
OPTIONS:
--query=QUERY Business type to search for (required)
--zips=ZIP,ZIP Comma-separated zip codes to search
--cities=CITY,CITY Comma-separated cities to search
--state=XX Search all zip codes in a US state (2-letter code)
--min-pop=N Filter zips to those with population >= N (use with --state)
--limit=N Max results per location (default 20, max 20)
--output=PATH Output CSV path (default ./output/results.csv)
ENVIRONMENT:
RAPIDAPI_KEY Your RapidAPI key (required)
Get one at: https://rapidapi.com/alexanderxbx/api/maps-data
EXAMPLES:
# All pizza places in California (zips with pop >= 5000)
npm run scrape -- --query="pizza restaurant" --state=CA --min-pop=5000
# Dentists in specific NYC zip codes
npm run scrape -- --query="dentist" --zips=10014,10013,10012
# Gyms across Texas cities
npm run scrape -- --query="gym" --cities="Austin TX,Dallas TX,Houston TX"
`);
process.exit(1);
}
const apiKey = process.env.RAPIDAPI_KEY;
if (!apiKey) {
console.error('Error: RAPIDAPI_KEY environment variable is required');
console.error('Get your key at: https://rapidapi.com/alexanderxbx/api/maps-data');
process.exit(1);
}
// Build location list from all sources
const locations: string[] = [];
if (zips) {
locations.push(...zips.split(',').map(s => s.trim()));
}
if (cities) {
locations.push(...cities.split(',').map(s => s.trim()));
}
if (state) {
const minPopNum = minPop ? parseInt(minPop, 10) : 0;
const stateZips = minPopNum > 0
? getZipsByMinPopulation(minPopNum, state)
: getZipsByState(state);
locations.push(...stateZips.map(z => z.zip));
console.log(`Loaded ${stateZips.length} zip codes for ${state.toUpperCase()}${minPopNum > 0 ? ` (pop >= ${minPopNum.toLocaleString()})` : ''}`);
}
if (locations.length === 0) {
console.error('No locations to search. Provide --zips, --cities, or --state.');
process.exit(1);
}
console.log(`Scraping "${query}" across ${locations.length} location(s)...\n`);
const client = new GoogleMapsClient({ apiKey, requestsPerSecond: 2 });
const allPlaces: Place[] = [];
const start = Date.now();
for (let i = 0; i < locations.length; i++) {
const loc = locations[i];
console.log(` [${i + 1}/${locations.length}] Searching: ${query} in ${loc}`);
try {
const results = await client.search({
query: `${query} in ${loc}`,
limit,
country: 'us',
});
allPlaces.push(...results);
console.log(` Found ${results.length} results`);
} catch (err: any) {
console.error(` Error: ${err.message}`);
}
}
// Deduplicate
const unique = dedup(allPlaces);
console.log(`\nTotal: ${allPlaces.length} results, ${unique.length} unique after dedup`);
// Export
await exportCSV(unique, output);
console.log(`Saved to: ${output}`);
const duration = ((Date.now() - start) / 1000).toFixed(1);
console.log(`Done in ${duration}s`);
// Print sample
if (unique.length > 0) {
console.log('\nSample result:');
const sample = unique[0];
console.log(` ${sample.name}`);
console.log(` ${sample.address}`);
if (sample.phone) console.log(` ${sample.phone}`);
if (sample.website) console.log(` ${sample.website}`);
if (sample.rating) console.log(` ${sample.rating} stars (${sample.reviews_count} reviews)`);
}
}
main().catch(err => {
console.error('Fatal:', err.message);
process.exit(1);
});# Search pizza places in 3 NYC zip codes
npm run scrape -- --query="pizza restaurant" --zips=10014,10013,10012
# Search ALL dentists in Texas (zips with population >= 5000)
npm run scrape -- --query="dentist" --state=TX --min-pop=5000
# Every gym in California (all 2,657 zip codes — takes a while)
npm run scrape -- --query="gym" --state=CA
# Search dentists across specific cities
npm run scrape -- --query="dentist" --cities="Austin TX,Dallas TX,San Antonio TX"
# Custom output file
npm run scrape -- --query="gym" --zips=90210 --output=./data/gyms.csvimport { GoogleMapsClient } from './client.js';
const client = new GoogleMapsClient({
apiKey: process.env.RAPIDAPI_KEY!,
requestsPerSecond: 2,
});
// Simple search
const places = await client.search({
query: 'coffee shop in 94105',
limit: 20,
});
// Search with coordinates
const { lat, lng } = await client.geocode('94105');
const nearby = await client.search({
query: 'coffee shop',
lat,
lng,
zoom: 14,
limit: 20,
});If you want a browser UI instead of (or in addition to) the CLI, add Express:
npm install express
npm install -D @types/expressCreate src/server.ts:
import express from 'express';
import { GoogleMapsClient } from './client.js';
import { exportCSV } from './csv.js';
import { tmpdir } from 'os';
import { join } from 'path';
import { readFile, unlink } from 'fs/promises';
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const client = new GoogleMapsClient({
apiKey: process.env.RAPIDAPI_KEY!,
requestsPerSecond: 2,
});
// Simple HTML form
app.get('/', (_req, res) => {
res.send(`<!DOCTYPE html>
<html><head><title>Google Maps Scraper</title></head>
<body style="font-family:sans-serif;max-width:600px;margin:40px auto;padding:0 20px">
<h1>Google Maps Scraper</h1>
<form method="POST" action="/scrape">
<label>Search query:<br>
<input name="query" placeholder="pizza restaurant" style="width:100%;padding:8px;margin:4px 0 12px" required>
</label>
<label>Locations (comma-separated zips or cities):<br>
<input name="locations" placeholder="10014, 10013, 10012" style="width:100%;padding:8px;margin:4px 0 12px" required>
</label>
<button type="submit" style="padding:10px 24px;cursor:pointer">Scrape</button>
</form>
</body></html>`);
});
app.post('/scrape', async (req, res) => {
const { query, locations: locStr } = req.body;
const locations = locStr.split(',').map((s: string) => s.trim()).filter(Boolean);
const allPlaces: any[] = [];
for (const loc of locations) {
try {
const results = await client.search({ query: `${query} in ${loc}`, limit: 20 });
allPlaces.push(...results);
} catch {}
}
// Dedup
const seen = new Set<string>();
const unique = allPlaces.filter(p => { if (seen.has(p.place_id)) return false; seen.add(p.place_id); return true; });
// Export CSV and send as download
const tmpPath = join(tmpdir(), `scrape-${Date.now()}.csv`);
await exportCSV(unique, tmpPath);
const csv = await readFile(tmpPath, 'utf-8');
await unlink(tmpPath);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="maps-scrape-${Date.now()}.csv"`);
res.send(csv);
});
const port = parseInt(process.env.PORT || '3000', 10);
app.listen(port, () => console.log(`Scraper running at http://localhost:${port}`));Add a script to package.json:
{
"scripts": {
"scrape": "tsx src/index.ts",
"serve": "tsx src/server.ts"
}
}Run locally: npm run serve then open http://localhost:3000
RAPIDAPI_KEY in Railway's dashboardnpx tsx src/server.tsprocess.env.PORT and gives you a public URLYou can add password protection by checking a PASSWORD env var in the POST handler, or use Railway's built-in auth features.
This is a standard Node.js app. It runs anywhere:
fly launch, set secrets with fly secrets set RAPIDAPI_KEY=xxxFROM node:20-slim + npm install + npx tsx src/server.tsThe underlying API is the Maps Data API on RapidAPI: https://rapidapi.com/alexanderxbx/api/maps-data
| Endpoint | Purpose | Example |
|---|---|---|
searchmaps.php | Search businesses by query + location | ?query=pizza+in+10014&limit=20 |
geocoding.php | Convert address/zip to lat/lng | ?query=10014,+US |
nearby.php | Search near a lat/lng point | ?query=pizza&lat=40.73&lng=-74.00 |
place.php | Get full details for one business | ?place_id=ChIJ... |
The free tier on RapidAPI has request limits (check your plan). The client is hard-coded to 2 requests/second with automatic retries on 429s. Adjust requestsPerSecond if your plan allows more.
Each result includes:
place_id — unique Google Maps identifiername — business nameaddress — full street addressphone — phone number (if listed)website — website URL (if listed)rating — star rating (1-5)reviews_count — number of Google reviewslat / lng — coordinatestypes / category — business categories (e.g., "pizza_restaurant")place_id — the same business often shows up in adjacent zip code searches.types field tells you what kind of business it is. Use it to filter out irrelevant results (e.g., filter out "bar" when searching for "restaurant").Run /icp-prompt-builder on a 50-business sample (required step above). Then /blitz-list-builder with the filtered domains to find owner contacts — Google Maps returns businesses, not people.
After owner discovery: /list-builder (Phase 5, emails) to fill missing emails, then /list-quality-scorecard to grade.
Or wait: if your scrape returned <200 businesses, your query + location is too narrow. Widen before proceeding.
/icp-prompt-builder — required qualification pass/blitz-list-builder — find owner contacts at each business/list-builder (Phase 5, emails) — fill missing emails/list-quality-scorecard — grade the final list© growthenginenowoslawski, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/google-maps-list-builder of growthenginenowoslawski/coldoutboundskills.
Open the folder on GitHubat commit 25c5d85
Google Maps List Builder next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Google Maps List Builder this skillgrowthenginenowoslawski/coldoutboundskills | 753 | — | ~6.2k | Automated safety check: Notes | MIT | |
| Cold Email Local Businessgmapsscraper/google-maps-agent-skills | 132 | — | ~1.2k | Automated safety check: Pass | MIT | |
| Google Maps Reviews Scrapergmapsscraper/google-maps-agent-skills | 132 | — | ~1.2k | Automated safety check: Pass | MIT | |
| Apify Buying Signal Detectionapify/awesome-skills | 266 | — | ~5.1k | Automated safety check: Notes | Apache-2.0 | |
| Google Maps API Skillbrowser-act/skills | 6.1k | 1 repos | ~1.4k | Automated safety check: Pass | MIT | |
| Business Email Extractorgmapsscraper/google-maps-agent-skills | 132 | — | ~1.1k | Automated safety check: Pass | MIT |
gmapsscraper/google-maps-agent-skills
Write hyper-personalized cold emails to local businesses using their Google Maps data (CSV).
gmapsscraper/google-maps-agent-skills
Analyze Google Maps review data from CSV exports. An agent skill from gmapsscraper/google-maps-agent-skills.
apify/awesome-skills
Set up a recurring buying-signal detection pipeline that finds companies showing buying intent across three signal types — job postings (hiring for the persona), fundraising events (recent raises)…
browser-act/skills
This skill helps users automatically scrape business data from Google Maps using the BrowserAct Google Maps API.
gmapsscraper/google-maps-agent-skills
Extract verified business email addresses from Google Maps listings.
gmapsscraper/google-maps-agent-skills
Export Google Maps business data to CSV, JSON, or CRM format (HubSpot, Pipedrive, Salesforce).
growthenginenowoslawski/coldoutboundskills
Diagnostic audit for a running cold email program. An agent skill from growthenginenowoslawski/coldoutboundskills.
growthenginenowoslawski/coldoutboundskills
Conversational intake for cold email campaigns. An agent skill from growthenginenowoslawski/coldoutboundskills.
growthenginenowoslawski/coldoutboundskills
META skill — build the largest possible qualified lead list for any request, end to end.
growthenginenowoslawski/coldoutboundskills
Autonomous cold email campaign launcher. An agent skill from growthenginenowoslawski/coldoutboundskills.
growthenginenowoslawski/coldoutboundskills
Use the Blitz API to find decision-makers at specific companies when you already have a list of company domains.
growthenginenowoslawski/coldoutboundskills
Compare reply rates, bounce rates, and positive reply rates broken down by inbox type (SMTP / Gmail / Outlook) for a Smartlead account.
Works with
Categories
Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment. Google Maps List Builder is an agent skill from growthenginenowoslawski/coldoutboundskills. Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment.
Google Maps List Builder fits situations like: tasks that involve Cold outreach; tasks that involve Web scraping; tasks that involve CSV and tabular files.
Run `npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a claude-code`. Or copy the skill folder (skills/google-maps-list-builder in growthenginenowoslawski/coldoutboundskills) into .claude/skills/google-maps-list-builder in your project. Claude Code loads it when a task matches its description.
Run `npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a codex`. Or copy the skill folder (skills/google-maps-list-builder in growthenginenowoslawski/coldoutboundskills) into .agents/skills/google-maps-list-builder in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/google-maps-list-builder, .gemini/skills/google-maps-list-builder, .github/skills/google-maps-list-builder and .opencode/skills/google-maps-list-builder in your project.
Going by SKILL.md and its folder, Google Maps List Builder needs the command-line tools its instructions call (npm, npx and fly) and credentials named RAPIDAPI_KEY. Our summary lists: Node.js; A credential in RAPIDAPI_KEY.
SKILL.md names 1 domain. In commands or code: rapidapi.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Google Maps List Builder is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.2k tokens (SKILL.md is roughly 25k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Google Maps List Builder: Cold Email Local Business (gmapsscraper/google-maps-agent-skills, 132 stars), Google Maps Reviews Scraper (gmapsscraper/google-maps-agent-skills, 132 stars), Apify Buying Signal Detection (apify/awesome-skills, 266 stars) and Google Maps API Skill (browser-act/skills, 6.1k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
growthenginenowoslawski (a GitHub user) maintains it in growthenginenowoslawski/coldoutboundskills, which has 753 GitHub stars. The repository holds 49 skills in this directory. The repository was last updated on October 5, 2026.
Source: growthenginenowoslawski/coldoutboundskills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.