Package overview
Overview
Searches Google Maps for businesses in the areas you pick, researches each one, scores it against what you sell, and gives you a ranked call list with an opening line already written. Run it once or put it on a schedule.
What you need
| Connection | Required | Why | Where to get it |
|---|---|---|---|
google-places |
yes | Finds the businesses | Google Maps Platform credentials |
anthropic |
yes | Does the research and writes the opening lines | Anthropic Console keys |
smtp |
no | Only for the emailed report. Skip it and leads still get saved | Your own mail provider |
For the Google key: create the key, enable Places API (New) on the project, and turn on billing. Google will not serve requests without billing enabled even inside the free tier, and the failure looks like a permissions error rather than a billing one. Setup and pricing are worth a skim before your first large run.
It also creates one table called leads, which is where the results go.
Set it up
Three fields decide what you get back. They go into every research prompt and into the score, so getting them right matters more than anything else here.
| Field | What to put |
|---|---|
offerDescription |
What you sell, in a sentence or two |
idealCustomer |
What a good customer looks like: size, type, situation |
painCriteria |
What a business would be complaining about if they needed you |
Then tell it where to look.
| Field | What to put |
|---|---|
serviceTypes |
The kinds of business to search for. Each one is searched in each location |
searchLocations |
Where to search. A name on its own is enough: { "name": "Gastonia, NC" } |
maxLeadsPerRun |
How many new businesses to research per run. This is your cost control |
minLeadScore |
Only keep leads at or above this score |
notifyEmailTo |
Who gets the report. Leave empty for no email |
Start with maxLeadsPerRun at 10 and one location. A run costs a few cents per business researched, and you want to read the first batch before you spend on a hundred more.
Run it
In the sidebar, open Packages → Lead Generation, then click the workflow called lead-gen. That is the one you run. The others in the package are pieces it calls, and you never open them directly unless you want to change how they work.
Put your values in the Test data panel and press Run. Here is a complete example to edit:
{
"serviceTypes": ["med spas", "barbers", "nail salons"],
"searchLocations": [{ "name": "Gastonia, NC" }],
"maxLeadsPerRun": 10,
"minLeadScore": 0,
"notifyEmailTo": [],
"offerDescription": "24/7 AI call answering and appointment scheduling for local service businesses, so they never miss a customer call or booking.",
"idealCustomer": "Established local service businesses, roughly 5-50 employees, with steady call volume and limited after-hours coverage.",
"painCriteria": "Missed or unreturned calls, slow response times, no after-hours availability, scheduling difficulties, long hold times, poor phone communication."
}
Everything below notifyEmailTo is what you change to point this at your own offer. The scoring fields (scoringWeights, idealEmployeesMin, idealEmployeesMax, maxEmployees, excludeBusinessTypes) can be left out entirely until you need them, and are covered under Advanced.
A location name on its own is usually all you need. Google reads it the same way it would if you typed it into Maps, so "Gastonia, NC", "Charlotte metro" and "78704" all work.
If you want to control the exact area instead of letting Google decide, add coordinates and a radius:
{
"name": "Gastonia, NC",
"latitude": 35.2621,
"longitude": -81.1873,
"radiusMiles": 25
}
radiusMiles caps at 31, which is Google's limit, and is ignored unless both coordinates are set. To find coordinates, search the place on Google Maps and read them out of the URL.
After the run finishes, your leads show up in the Lead Generation dashboard. Start with the Call List at the bottom: it is sorted by score, so the businesses worth phoning first are at the top, and each row carries the phone number and the opening line.
Read those leads before doing anything else. If they look wrong, the fix is almost always in the three fields above, not in the scoring.
What happens on a run
- Searches each business type in each location.
- Drops anything already in your
leadstable, before doing any research on it. Re-running costs nothing for businesses you already have. - Researches each new business: who runs it, how big it is, and what its reviews say.
- Scores it 0-100 and marks it High, Medium, Low, or Disqualified.
- Saves the ones above your threshold and emails you the batch.
Every lead you get back has the business details, whoever the research turned up as the decision-maker, a summary of what its reviews complain about, the score with a breakdown of how it was reached, and an opening line written for that specific business.
If the leads look wrong
Wrong businesses entirely. Your serviceTypes are too broad. Searching "call center" on Google Maps returns government offices, banks and shipping stores, because Maps categorises loosely. Use narrower terms, then add the categories you keep seeing to excludeBusinessTypes (a comma-separated list) to drop them.
Right businesses, wrong order. The default scoring is built for consumer businesses like dentists and plumbers, where Google reviews are plentiful and revealing. If you sell to other businesses, reviews tell you almost nothing and the scoring needs changing. See below.
Everything scores about the same. The research could not find much about any of them. That is common for small private companies, and it is honest: the score does not pretend to separate businesses it has no information about.
Once it's working
Everything so far has been you pressing Run. These three turn it into something that feeds you leads without being asked.
Run it every day
Add a schedule trigger to the lead-gen workflow and pick a time. Each morning it searches the same areas again and only pays attention to businesses that were not there yesterday.
This is cheap for the same reason re-runs are cheap: the dedupe happens before any research, so a day where nothing new opened costs you a Google search and nothing else. New businesses appear constantly, and a daily run means you hear about them within a day of them showing up on Maps rather than whenever you next remember to look.
Pair it with minLeadScore so a quiet day stays quiet instead of emailing you three businesses you would never call.
Send new leads wherever you actually look
The default emails you a report. Most people do not read email that closely, and a lead you notice on Thursday for a business that opened Monday is a worse lead.
on-new-leads is an extension point, which means you can hand it your own workflow at install time and the package will call that instead. It receives the new leads, how many there are, and the notify list. What you do with them is entirely yours: post to a Discord channel, drop a Slack message in the room your sales people already sit in, push straight into your CRM, fire a webhook at your own backend, or load them into a dialer.
The contract is small. Build a workflow that accepts those three inputs, do whatever you want inside it, and bind it to on-new-leads. Nothing else in the package changes.
Call it from your own app
Add a webhook trigger to lead-gen and you get a URL you can POST to. Whatever JSON you send becomes the run's input, exactly like the Test data panel, so you can drive different searches per call:
curl -X POST "<your-webhook-url>" \
-H "Content-Type: application/json" \
-d '{
"serviceTypes": ["dental offices"],
"searchLocations": [{ "name": "Austin, TX" }],
"maxLeadsPerRun": 25
}'
That turns lead generation into something your own product can ask for. A user picks a city in your app and you kick off a search for that city. A new client signs up and you run their territory. A sales rep types a business type into an internal tool and gets a call list back, without anyone opening QuickFlo at all.
Advanced
How the score works
The score is calculated in code, so the same business always gets the same number, and score_reasoning on every lead shows exactly which signals contributed what.
Each signal produces a value between 0 and 1, and scoringWeights decides how much each one counts. The weights get divided by their own total, so they do not have to add up to 100. Setting one to 0 turns that signal off.
| Signal | Default | What it measures |
|---|---|---|
reviewVolume |
25 | Number of reviews, as a rough proxy for how many customers they deal with |
reviewPain |
40 | How closely the reviews match your painCriteria |
afterHoursGap |
10 | Opening hours suggest nobody covers nights or weekends |
missedCallMentions |
15 | Reviewers saying their calls went unanswered |
hasWebsite |
5 | Has a website |
hasPhone |
5 | Has a phone number |
employeeFit |
0 | Headcount versus idealEmployeesMin and idealEmployeesMax |
yearsInBusiness |
0 | Ten years or more scores full marks |
hiringSignal |
0 | Currently posting jobs |
adSpendSignal |
0 | Currently running ads |
contactConfidence |
0 | How sure the research was about the decision-maker it named |
Two behaviours are worth knowing about before you retune.
When the research cannot find something, it scores in the middle rather than at zero. Headcount and founding dates are frequently unavailable for small private companies. If a blank counted against a business, the only ones able to score well on those signals would be large public companies, which is the opposite of what most people want.
maxEmployees and excludeBusinessTypes remove a business from the results rather than scoring it low. If a search returns AT&T, that is not a weak lead, it is the wrong result, and giving it a low score still leaves it in your list. Removed businesses come back marked Disqualified with the reason.
Scoring for businesses that sell to businesses
Review-based signals do not work here. An answering service's customers do not review it on Google Maps, so reviewVolume and reviewPain become noise and the ranking drifts toward whichever company is biggest. Shift the weight onto company facts instead:
{
"scoringWeights": {
"reviewVolume": 5,
"reviewPain": 10,
"afterHoursGap": 0,
"missedCallMentions": 5,
"hasWebsite": 5,
"hasPhone": 5,
"employeeFit": 30,
"yearsInBusiness": 15,
"hiringSignal": 15,
"adSpendSignal": 10,
"contactConfidence": 0
},
"idealEmployeesMin": 25,
"idealEmployeesMax": 500,
"maxEmployees": 2000,
"excludeBusinessTypes": "government, bank, corporate office, courier"
}
idealEmployeesMin and idealEmployeesMax set the headcount range you want. Businesses inside it score full marks on employeeFit, smaller ones score proportionally, and larger ones fall away quickly.
Replacing the research or the handoff
Two steps are built to be swapped, and each ships with a working version.
enrich-lead turns one search result into a scored lead. Replace it to use your own data provider or your own scoring. Yours has to return the same fields, including lead_score and priority.
on-new-leads runs when a batch finds something. It receives { leads, leadCount, notifyEmailTo }, where leads is the full enriched records, highest score first. The default sends an HTML report with a CSV attached. See "Send new leads wherever you actually look" above for what to do with it.
If you only want to change the numbers, the weights live in the score-lead step and can be edited directly.
Table and outputs
The leads table is keyed on each business's Google Places ID, which is what makes the deduplication reliable across runs. A run returns { found, candidates, new, leadCount, notified }.