AGENT SKILLBY APIFY
Verified email finder
AT A GLANCE
Verified email finder is an email agent skill from apify. Builds a list of verified business emails from Google Maps, Google SERPs, or a user-supplied URL list. The apify/awesome-skills repository on GitHub has 246 stars and the Apache-2.0 license. To add it, run npx skills add apify/awesome-skills --skill apify-verified-email-finder
- SOURCE REPOSITORY
- apify/awesome-skills (opens GitHub in a new tab)
- REPO STARS
- SOURCE REPOSITORY
- LICENSE
- Apache-2.0
- SKILL.MD SIZE
- 27.5 KB568 LINES
QUICK ANSWER
- WHAT IT IS01
- Builds a list of verified business emails from Google Maps, Google SERPs, or a user-supplied URL list. Verification happens inside the same Apify run, no third-party verifier needed. It brings Apify's specific operating context into email marketing, so the agent is guided by a sharper source than a generic prompt.
- INPUTS02
- Context for email marketing: your goals, audience, constraints, and any source material the skill asks for.
- OUTPUT03
- A ready-to-use result for email marketing: the analysis, copy, or recommendations the agent produces.
INSTALL
One command adds this skill from its source repository.
npx skills add apify/awesome-skills --skill apify-verified-email-finderREAD THE SKILL.MD BELOW BEFORE YOU RUN IT. SKILLS INSTRUCT YOUR AGENT.
SKILL.MD
The full file, as published in the source repository. Copy it, download it or open it on GitHub.
ON THIS PAGE · 42
Verified Email Finder#
Return a list of verified business emails by routing the user's input to the right Apify Actor and turning on the leads enrichment + email verification add-ons in a single run. No third-party verifier (Hunter, NeverBounce, Apollo) needed — verification happens inside the same Actor run.
Prerequisites#
(No need to check it upfront)
The skill supports two execution paths. Pick the one that matches your environment — Steps 4 and 5 show commands for both.
MCP path (default in Claude sessions, recommended). If the Apify MCP server is connected, no setup is needed — auth runs through the user's Apify account. Use the call-actor and get-dataset-items MCP tools.
Script path (CLI / scheduled / non-Claude execution). Requires:
.envfile withAPIFY_TOKEN- Node.js 20.6+ (for native
--env-filesupport)
Workflow#
Copy this checklist and track progress:
Task Progress:
- [ ] Step 1: Collect the six required anchor inputs
- [ ] Step 2: Route to the correct Actor (confirm if ambiguous)
- [ ] Step 3: Build the Actor input (verification always ON)
- [ ] Step 4: Run the Actor and wait
- [ ] Step 5: Apply the result-scope filter, deduplicate, and renderStep 1: Collect the Six Required Anchor Inputs#
Ask all six as one block before any Actor call. Don't bundle Actor-specific optional fields (country code, language, max pages) into this round — surface those as follow-ups.
- What do you have to start with? —
location query/SERP keyword/URL list. This drives the routing decision. - The actual input — the location string, the keyword(s), or the URLs themselves.
- Department filter — one or more of:
c_suite,product,engineering_technical,design,education,finance,human_resources,information_technology,legal,marketing,medical_health,operations,sales,consulting. Default isany(leave the array empty), but ask every time. - Max contacts per domain / business — passed as
maximumLeadsEnrichmentRecords. Default3, but ask every time. - Output format —
CSVorJSON. Ask every time. - Result scope — which leads to keep in the deliverable. The Actor always runs the same way (verification always on); this only controls post-run filtering. Pick one:
verified-only(default) — only leads withemailVerification.result == "ok". Safest for cold email.verified-plus-catchall—okpluscatch_all. Catch-all is often deliverable but unprovable.all-emails— any lead with a non-emptyemail, regardless of verification.with-phone— any lead with a non-empty phone number, regardless of email status. Use for call campaigns.everything— every lead the Actor returned, even incomplete ones.
Step 2: Route to the Correct Actor#
Inspect anchor #1 and pick the Actor.
| User has to start with | Actor ID | Use when |
|---|---|---|
| Location + business type ("dentists in Berlin") | compass/crawler-google-places | Local leads list from Maps listings; best when user wants address / phone / hours too |
| Keyword / search query ("best CRM software") | apify/google-search-scraper | Contacts from whichever sites Google ranks for a topic |
| Pre-existing URL list (pasted, file path) | vdrmota/contact-info-scraper | User already has domains; cheapest route since no discovery step |
All three Actors share the same three add-on fields, so verification behavior is identical across routes.
Decision examples
| User says | Route |
|---|---|
| "Dentists in Munich" / "Lawyers in Prague" | Maps |
| "Marketing contacts at the top results for 'AI agent builder'" | Search |
| "Find emails for these 5 URLs: acme-co.example, demo-co.example..." | URL list |
| "Find HR contacts at Fortune 500 companies" | Ask: SERP for "Fortune 500 HR" or a URL list? |
| "Find contacts at SaaS companies in Berlin" | Ask: Maps for "SaaS companies in Berlin" or SERP for "SaaS companies Berlin"? Maps works best when businesses are Google-Maps-listed. |
| (User pastes both a SERP keyword AND a URL list) | Ask: run one route, the other, or both as separate deliverables? |
Ambiguity rule: if anchor #1 is unclear, ask one follow-up before running. Never burn Actor compute on a guessed route.
Mixed deliverables: if the user explicitly asks for two routes in one deliverable, run both Actors and concatenate. The Source column makes the mix clear; dedupe by email across the combined output.
Step 3: Build the Actor Input#
Always set these three fields, regardless of which Actor is selected.
| Field | Value |
|---|---|
maximumLeadsEnrichmentRecords | anchor #4 (default 3, min 1) |
leadsEnrichmentDepartments | anchor #3 as array, or [] if "any" |
verifyLeadsEnrichmentEmails | true (always) — guard rail, never set to false |
Full per-Actor input parameters and example payloads are in reference/apify-actor-usage.md (opens in a new tab).
URL-list pre-validation: before submitting URLs to vdrmota/contact-info-scraper, parse each one and check it is http/https and parseable. Skipped entries must appear in the output as skipped — invalid URL, never silently dropped.
Step 4: Run the Actor#
Maps and SERP runs with leads enrichment can take several minutes per query. Raise the timeout for large jobs.
MCP path (default in Claude sessions):
Call the call-actor tool:
actor: one of the three Actor IDs (compass/crawler-google-places,apify/google-search-scraper,vdrmota/contact-info-scraper)input: the JSON payload from Step 3callOptions:{"timeout": 1800, "memory": 4096}for a generous budget
The tool returns runId and datasetId. If status is still RUNNING, poll with get-actor-run (waitSecs up to 45) until SUCCEEDED. Capture both IDs for the run_metadata.json sidecar.
Script path (CLI / scheduled use):
node --env-file=.env ${CLAUDE_PLUGIN_ROOT}/reference/scripts/run_actor.js \
--actor "ACTOR_ID" \
--input 'JSON_INPUT' \
--output YYYY-MM-DD_verified-emails.csv \
--format csv \
--timeout 900Use --format json for JSON. The script writes the raw dataset to disk; Step 5 still applies the spurious-match + scope filters on top.
Step 5: Filter, Deduplicate, and Render#
Pull the dataset:
- MCP path: call
get-dataset-itemswith thedatasetIdfrom Step 4. Use thefieldsparameter (e.g.,title,searchString,countryCode,city,address,phone,website,leadsEnrichment) andclean: trueto keep the response small. For datasets that still exceed the response cap, fetch directly viacurl https://api.apify.com/v2/datasets/<id>/items?fields=...&clean=trueand pipe throughjq. - Script path: the raw dataset is already on disk in the file from Step 4.
Each record contains business fields plus a leadsEnrichment array (Maps, SERP) or top-level lead fields (URL list). Each lead has a departments array, a companyWebsite, and an emailVerification object with result (ok / invalid / disposable / catch_all / unknown / error) and quality (good / risky / bad).
-
Spurious-match filter (mandatory, always on). Apply this first, before any other filter. The lead-enrichment service can return global-fallback leads when no local match exists (real case observed: a single US-zoo CFO whose
companyWebsite=zoo.orgwas attributed to 8 unrelated Polish zoos because the matcher latched onto thezoosubstring). Drop any lead whosecompanyWebsitehostname doesn't equal the source URL's hostname (striphttps?://, leadingwww., anything after/; lowercase). Count drops inrun_metadata.jsonand call them out in the deliverable header if non-zero. -
Filter by result scope (anchor #6). Applied second.
Scope Row-keep logic verified-onlyemailVerification.result == "ok"verified-plus-catchallemailVerification.result in {"ok", "catch_all"}all-emailsemailis non-empty (any result, including missing verification)with-phonephone(or company phone) is non-empty (regardless of email)everythingkeep every lead, no filter -
Dedupe: group by lowercased email; keep the first occurrence and merge
Source Query or URLif the same email appears from multiple sources. Forwith-phonerows that have no email, dedupe by lowercased phone instead. -
Empty-result surfacing: if the department filter (anchor #3) produces zero leads for a given domain, include a row for that domain with
Email = ""andEmail Verification Status = "no leads matched filter". Do not silently drop it. This is separate from the result-scope filter above — empty-domain rows are inserted before scope-filtering and always shown.
Output row schema (16 columns, including Departments) and per-format rendering details are in reference/output-formats.md (opens in a new tab).
Worked Examples#
- Maps: examples/example-maps-input.md (opens in a new tab)
- SERP: examples/example-search-input.md (opens in a new tab)
- URL list: examples/example-url-list-input.md (opens in a new tab)
Quality Rules (always enforce)#
- Guard rail: never submit a run with
verifyLeadsEnrichmentEmails: false. - Provenance & traceability: populate the
Sourcecolumn on every row; carry ApifyrunId+datasetIdinrun_metadata.json. - No fabrication: missing dataset fields stay blank.
- Deliverable header transparency: state the active result scope and the spurious-match drop count; offer to re-render under a different scope.
- Ambiguity confirm: if anchor #1 is unclear, ask before running.
Cost & Pricing#
Email verification is charged only for decisive results (ok / invalid / disposable); catch_all / unknown / error are free. Leads enrichment is charged per successfully extracted lead. Check the Apify console for live rates (they vary by subscription tier and change over time).
Error Handling#
See reference/troubleshooting.md (opens in a new tab).
Supporting file: examples/example-maps-input.md#
Example — Maps route ("dentists in Berlin")#
Anchors#
| # | Value |
|---|---|
| 1 What do you have | location query |
| 2 Input | dentists in Berlin |
| 3 Departments | marketing, c_suite |
| 4 Max contacts | 3 |
| 5 Format | CSV |
| 6 Scope | verified-only |
Optional follow-ups: language=en, maxCrawledPlacesPerSearch=20, scrapePlaceDetailPage=true (so address / phone come back).
Routing: unambiguous → compass/crawler-google-places.
Actor input#
{
"searchStringsArray": ["dentists"],
"locationQuery": "Berlin, Germany",
"maxCrawledPlacesPerSearch": 20,
"language": "en",
"scrapePlaceDetailPage": true,
"maximumLeadsEnrichmentRecords": 3,
"leadsEnrichmentDepartments": ["marketing", "c_suite"],
"verifyLeadsEnrichmentEmails": true
}Run it via the MCP call-actor tool or the script (SKILL.md Step 4). expected_leads = 20 × 3 = 60 — under the 200-lead warning threshold, no confirm needed.
Sample output#
| Source | Business | Full Name | Job Title | Seniority | Status | Quality | Phone | City | |
|---|---|---|---|---|---|---|---|---|---|
| Maps | Example Dental A | Sample Contact 1 | Marketing Lead | manager | contact1@dental-a.example | ok | good | +49 30 5550001 | Berlin |
| Maps | Example Dental B | Sample Contact 2 | Owner | c_suite | contact2@dental-b.example | ok | good | +49 30 5550002 | Berlin |
| Maps | Example Dental C | Sample Contact 3 | CMO | c_suite | contact3@dental-c.example | ok | good | +49 30 5550003 | Berlin |
Deliverable header: Scope verified-only, spurious-match drops: 0. Ask to re-render under a wider scope to include catch-all / unknown. A run_metadata.json sidecar is written next to the CSV.
Supporting file: examples/example-search-input.md#
Example — SERP route ("best CRM software")#
Anchors#
| # | Value |
|---|---|
| 1 What do you have | SERP keyword |
| 2 Input | best CRM software |
| 3 Departments | sales |
| 4 Max contacts | 3 |
| 5 Format | JSON |
| 6 Scope | verified-only |
Optional follow-ups: countryCode=us, languageCode=en, maxPagesPerQuery=1.
Routing: unambiguous → apify/google-search-scraper.
Actor input#
{
"queries": "best CRM software",
"maxPagesPerQuery": 1,
"countryCode": "us",
"languageCode": "en",
"maximumLeadsEnrichmentRecords": 3,
"leadsEnrichmentDepartments": ["sales"],
"verifyLeadsEnrichmentEmails": true
}Run via MCP call-actor or the script (SKILL.md Step 4). expected_leads ≈ 10 × 3 = 30 — well under the warning threshold.
Sample output (truncated to one contact)#
{
"runMetadata": {
"runId": "AbCdEfGhIjK",
"datasetId": "LmNoPqRsTuV",
"actor": "apify/google-search-scraper",
"finishedAt": "2026-05-18T13:02:11Z",
"consoleUrl": "https://console.apify.com/actors/runs/AbCdEfGhIjK"
},
"filter": {"scope": "verified-only", "rowKeepLogic": "emailVerification.result == 'ok'"},
"contacts": [
{
"source": "Search",
"sourceQueryOrUrl": "best CRM software",
"business": "acme-crm.example",
"fullName": "Sample Contact 1",
"jobTitle": "Account Executive",
"seniority": "manager",
"email": "contact1@acme-crm.example",
"emailVerificationStatus": "ok",
"emailVerificationQuality": "good",
"linkedin": "http://www.linkedin.com/in/example-user-1",
"businessWebsite": "https://www.acme-crm.example",
"dateScraped": "2026-05-18T13:02:11Z"
}
/* + 2 more, schema in reference/output-formats.md */
]
}A run_metadata.json sidecar is written next to the JSON output.
Supporting file: examples/example-url-list-input.md#
Example — URL-list route (5 URLs in)#
Anchors#
| # | Value |
|---|---|
| 1 What do you have | URL list |
| 2 Input | https://acme-pay.example, https://demo-card.example, https://sample-bank.example, htp://broken-url, https://example-biz.example |
| 3 Departments | any (empty array) |
| 4 Max contacts | 2 |
| 5 Format | CSV |
| 6 Scope | with-phone (call campaign — keep any lead that has a phone, regardless of email) |
Optional follow-ups: maxRequestsPerStartUrl=10, maxDepth=2, mergeContacts=true.
Routing: unambiguous → vdrmota/contact-info-scraper.
Pre-validation#
htp://broken-url is skipped (scheme not http/https) → emitted as a skipped — invalid URL row. The other four URLs go to the Actor.
Actor input (only valid URLs)#
{
"startUrls": [
{"url": "https://acme-pay.example"},
{"url": "https://demo-card.example"},
{"url": "https://sample-bank.example"},
{"url": "https://example-biz.example"}
],
"maxRequestsPerStartUrl": 10,
"maxDepth": 2,
"mergeContacts": true,
"proxyConfig": {"useApifyProxy": true},
"maximumLeadsEnrichmentRecords": 2,
"leadsEnrichmentDepartments": [],
"verifyLeadsEnrichmentEmails": true
}Run via MCP call-actor or the script (SKILL.md Step 4). expected_leads = 4 × 2 = 8 — well under the threshold.
Sample output#
Under with-phone scope, every row needs a non-empty phone — verification status is informational, not a gate.
| Source | Business | Full Name | Job Title | Status | Quality | Phone | |
|---|---|---|---|---|---|---|---|
| URL list | acme-pay.example | Sample Contact 1 | Head of BD | contact1@acme-pay.example | ok | good | +1 415 555 0101 |
| URL list | acme-pay.example | Sample Contact 2 | Director, Partnerships | contact2@acme-pay.example | catch_all | risky | +1 415 555 0102 |
| URL list | demo-card.example | Sample Contact 3 | VP Sales | contact3@demo-card.example | ok | good | +1 415 555 0201 |
| URL list | sample-bank.example | Sample Contact 4 | Growth Lead | unknown | +1 415 555 0301 | ||
| URL list | example-biz.example | Sample Contact 5 | Account Manager | contact5@example-biz.example | ok | good | +1 415 555 0401 |
| URL list | htp://broken-url | — | — | — | skipped — invalid URL | — | — |
Deliverable header: Scope with-phone, spurious-match drops: 0, 1 URL pre-skipped. Ask to re-render under verified-only to narrow.
Supporting file: reference/apify-actor-usage.md#
Apify Actor Usage#
Exact input parameters per Actor. Every payload includes the three shared add-on fields:
| Field | Value | Notes |
|---|---|---|
maximumLeadsEnrichmentRecords | anchor #4 (default 3, min 1) | 0 disables enrichment — never use. |
leadsEnrichmentDepartments | anchor #3 as array, or [] for any | Enum: c_suite, product, engineering_technical, design, education, finance, human_resources, information_technology, legal, marketing, medical_health, operations, sales, consulting. |
verifyLeadsEnrichmentEmails | true always | Guard rail. Adds emailVerification per lead. Never false. |
1. Google Maps — compass/crawler-google-places#
Anchor #1 is a location + business type.
| Field | Type | Required? | Notes |
|---|---|---|---|
searchStringsArray | string[] | yes | Business type(s), e.g. ["dentists"]. |
locationQuery | string | yes | Free-form location, e.g. "Berlin, Germany". |
maxCrawledPlacesPerSearch | int | optional, default 20 | Places per search string. |
language | string | optional | UI language, e.g. "en". |
countryCode | string | optional | ISO 3166 alpha-2. |
city, state, postalCode | string | optional | Narrower filters. |
scrapePlaceDetailPage | bool | optional, default false | Set true for address / hours / phone. |
skipClosedPlaces | bool | optional, default false | Drop permanently-closed listings. |
Example:
{
"searchStringsArray": ["dentists"],
"locationQuery": "Berlin, Germany",
"maxCrawledPlacesPerSearch": 20,
"language": "en",
"scrapePlaceDetailPage": true,
"maximumLeadsEnrichmentRecords": 3,
"leadsEnrichmentDepartments": ["marketing", "c_suite"],
"verifyLeadsEnrichmentEmails": true
}2. Google Search — apify/google-search-scraper#
Anchor #1 is a keyword.
| Field | Type | Required? | Notes |
|---|---|---|---|
queries | string | yes | Newline-separated queries, each ≤ 32 words. |
maxPagesPerQuery | int | optional, default 1 | Each page ≈ 10 results. |
countryCode | string | optional, default "us" | Drives the google.xx domain. |
languageCode | string | optional | UI language. |
searchLanguage | string | optional | lr filter — restricts result-page language. |
mobileResults | bool | optional, default false | Mobile SERP. |
Example:
{
"queries": "best CRM software",
"maxPagesPerQuery": 1,
"countryCode": "us",
"languageCode": "en",
"maximumLeadsEnrichmentRecords": 3,
"leadsEnrichmentDepartments": ["sales"],
"verifyLeadsEnrichmentEmails": true
}3. URL list — vdrmota/contact-info-scraper#
Anchor #1 is a pre-existing URL list.
| Field | Type | Required? | Notes |
|---|---|---|---|
startUrls | object[] | yes | [{"url": "https://..."}, ...]. Pre-validate as http/https; emit a skipped — invalid URL row for each rejection. |
proxyConfig | object | yes | Default {"useApifyProxy": true} works for most. |
maxRequestsPerStartUrl | int | optional, default 20 | Pages crawled per start URL. |
maxDepth | int | optional, default 2 | Link-depth from start. |
mergeContacts | bool | optional, default true | Merge per-domain contacts. Keep on. |
sameDomain | bool | optional, default true | Stay inside the start URL's domain. |
useBrowser | bool | optional, default false | Headless browser for JS-heavy sites; raises cost. |
Example:
{
"startUrls": [
{"url": "https://acme-co.example"},
{"url": "https://demo-co.example"}
],
"maxRequestsPerStartUrl": 10,
"maxDepth": 2,
"mergeContacts": true,
"proxyConfig": {"useApifyProxy": true},
"maximumLeadsEnrichmentRecords": 3,
"leadsEnrichmentDepartments": [],
"verifyLeadsEnrichmentEmails": true
}Supporting file: reference/output-formats.md#
Output Formats#
One row per contact (subject to anchor #6 scope), in CSV or JSON.
Row schema (16 columns)#
The lead object lives on leadsEnrichment[] for Maps and SERP; URL-list leads sit on the same field inside a per-domain merged record. Field names are the actual keys the Apify lead-enrichment service returns.
| Column | Source field |
|---|---|
Source | Literal: Maps, Search, or URL list (set by the route used) |
Source Query or URL | Original search string or input URL |
Business / Domain | title (Maps) / domain of url (SERP) / start URL host (URL list) |
Full Name | leadsEnrichment[].fullName |
Job Title | leadsEnrichment[].jobTitle |
Departments | leadsEnrichment[].departments joined with | (array of enum strings; often empty) |
Seniority | leadsEnrichment[].seniority (entry / manager / director / c_suite / etc., blank if unavailable) |
Email | leadsEnrichment[].email |
Email Verification Status | leadsEnrichment[].emailVerification.result (ok / invalid / disposable / catch_all / unknown / error) |
Email Verification Quality | leadsEnrichment[].emailVerification.quality (good / risky / bad) |
LinkedIn | leadsEnrichment[].linkedinProfile |
Phone | place phone if present (Maps) else leadsEnrichment[].companyPhoneNumber |
City | leadsEnrichment[].city (or place city for Maps) |
Country | leadsEnrichment[].country (or place countryCode for Maps) |
Business Address | place address (Maps only; blank for SERP / URL list) |
Business Website | place website / url / leadsEnrichment[].companyWebsite |
Date Scraped | run finish time (ISO 8601) |
Notes:
departmentsis a plural array on the lead (not a string field calleddepartment). Often empty, but populated for ~30% of leads with values like["marketing"]or["c_suite", "finance"]. Join with|for the CSV column.- Missing fields stay blank — never invent a value.
Filter & dedupe#
-
Spurious-match filter (mandatory, always on). The lead-enrichment service sometimes returns global-fallback leads when no local match exists — e.g., a US-zoo CFO with
companyWebsite=zoo.orgattributed to 8 unrelated Polish zoos because the matcher latched onto thezoosubstring.Row-keep logic: extract hostnames (strip
https?://, leadingwww., anything after/; lowercase) from both the source URL and the lead'scompanyWebsite, and keep only if both non-empty and equal. The source URL isplace.website(Maps), the SERP resulturl(Search), or the originalstartUrls[].url(URL list). If either hostname is empty, drop the lead.Count drops in
run_metadata.jsonunderstats.spuriousMatchesDroppedand surface in the deliverable header if non-zero. -
Result-scope filter (anchor #6). Applied after spurious-match.
Scope Row-keep logic verified-only(default)emailVerification.result == "ok"verified-plus-catchallemailVerification.result in {"ok", "catch_all"}all-emailsemailis non-empty (any result, including missing verification)with-phoneplace phoneorcompanyPhoneNumberis non-empty (regardless of email)everythingno filter -
Dedupe: for scopes that produce email rows, group by
email.toLowerCase(). Keep the first occurrence. If the same email comes from multiple sources, concatenateSource Query or URLwith|. Forwith-phonerows that have no email, dedupe by lowercased phone instead. -
Empty-result rows: if the department filter (anchor #3) produced zero leads for a given domain, include one row for that domain with blank
EmailandEmail Verification Status = "no leads matched filter". These rows are inserted before result-scope filtering and are always shown — they tell the user the filter narrowed too much. -
Invalid-URL rows (URL-list route only): include one row per pre-skipped URL with
Email Verification Status = "skipped — invalid URL". Also always shown.
State both the active result scope and the spurious-match drop count in the deliverable header.
Rendering#
Post-process the raw dataset (from get-dataset-items or the script's output file) the same way regardless of route:
- Flatten
leadsEnrichmentso each lead becomes its own row. - Apply both filters (spurious-match, then scope).
- Dedupe.
- Project to the 16 columns above (CSV) or to a
contactsarray (JSON).
CSV deliverable — one header row, one row per lead, trailing RUN_METADATA row: RUN_METADATA, runId=..., datasetId=..., actor=..., finishedAt=....
JSON deliverable — envelope:
{
"runMetadata": {"runId": "...", "datasetId": "...", "actor": "...", "finishedAt": "...", "consoleUrl": "https://console.apify.com/actors/runs/..."},
"filter": {"scope": "verified-only", "rowKeepLogic": "emailVerification.result == 'ok'"},
"contacts": [ /* ... */ ]
}Sidecar — always write run_metadata.json next to the deliverable with the same runMetadata fields plus stats (placesScraped, rawLeads, spuriousMatchesDropped, keptUnderScope). For multi-route deliverables, actor / runId / datasetId become arrays in matching order.
Supporting file: reference/scripts/package.json#
{ "type": "module" }
Supporting file: reference/troubleshooting.md#
Troubleshooting#
Auth / setup#
APIFY_TOKEN not found(script path only) — Create.envwithAPIFY_TOKEN=...; get the token fromhttps://console.apify.com/account/integrations. Not needed on the MCP path.Actor not found— Confirm the ID is one ofcompass/crawler-google-places,apify/google-search-scraper,vdrmota/contact-info-scraper. The script converts/to~when calling the API; expected.
Run-time#
Run FAILED— Open the console URL printed by the runner and read the Actor log. Most common cause: malformed input JSON.- Timeout — Leads enrichment adds 30–90 s per domain on top of the base scrape. Raise the timeout (try 1500–1800 s). If the run is still progressing, the dataset already has partial results — pull by datasetId.
Run TIMED-OUTfrom Apify — Lower source breadth (maxCrawledPlacesPerSearch,maxRequestsPerStartUrl, fewer queries).
Empty / weak results#
- No verified rows after filter — Re-render under
verified-plus-catchallorall-emailsscope. Catch-all SMTP servers can't be proven but often deliver. - No leads at all — Try in order: bump
maximumLeadsEnrichmentRecords; widenleadsEnrichmentDepartmentsto[]; confirm sources have reachable websites (Maps places without awebsitecan't be enriched). - All leads dropped by spurious-match filter — The enrichment service returned only global-fallback leads. There's no real fix — the source domain has no recognizable LinkedIn footprint. Surface the count and move on.
- Empty SERP queries — Confirm the keyword is non-empty and ≤ 32 words. Strip stray quotes.
URL-list route#
- Blocked domain — Set
useBrowser: trueinvdrmota/contact-info-scraperinput. Raises cost but unblocks most anti-bot sites. - Invalid URL — Pre-validate in Step 3; emit a
skipped — invalid URLrow. Never submit a bad URL to the Actor.
Routing ambiguity#
Ask one follow-up. Common patterns: SERP+URL-list pasted together → pick one or both; industry + no source → Maps or SERP?; industry + city without "Maps" → confirm route.
Cost surprises#
Pull the breakdown from the run console. Usual causes: maximumLeadsEnrichmentRecords too high, source breadth uncapped, useBrowser: true left on. Live rates are in the Apify console under the Actor's pricing tab; the 200-lead pre-submit warning rule lives in SKILL.md Step 3.
SHOWING THE FIRST PART OF 27.5 KB · THE FULL TEXT IS ON THIS PAGE
FAQ
07 QUESTIONS
01What is the Verified email finder agent skill?
Builds a list of verified business emails from Google Maps, Google SERPs, or a user-supplied URL list. Verification happens inside the same Apify run, no third-party verifier needed. It is listed under email and analytics in the 100xLab agent skills directory.
02How do I install Verified email finder in Cursor, Claude Code, or Codex?
Run npx skills add apify/awesome-skills --skill apify-verified-email-finder in the project where you want it, then ask your agent for the skill by name. The --skill flag installs only Verified email finder, not every skill in the repository.
03What does the Verified email finder SKILL.md cover?
The SKILL.md includes sections on Prerequisites, Workflow, Step 1: Collect the Six Required Anchor Inputs, Step 2: Route to the Correct Actor, Step 3: Build the Actor Input and Step 4: Run the Actor. Read the full text on this page or in plain markdown at /resources/agent-skills/apify-awesome-skills--apify-verified-email-finder--awglpb/raw.
04Which agents can use Verified email finder?
Verified email finder installs with the cross-agent skills installer (npx skills add), which sets it up for agents such as Claude Code, Cursor and Codex. Each agent loads the SKILL.md when you ask for the skill by name.
05What license is Verified email finder under?
The apify/awesome-skills repository declares the Apache-2.0 license. Read the license file in the repository before you copy or redistribute the skill.
06Is Verified email finder free to use?
Reading, copying and installing Verified email finder costs nothing: it is published in a public GitHub repository under the Apache-2.0 license. Reuse and redistribution follow that license.
07Where is the source of Verified email finder?
Verified email finder comes from https://github.com/apify/awesome-skills, which has 246 GitHub stars. The SKILL.md is at https://github.com/apify/awesome-skills/blob/HEAD/skills/apify-verified-email-finder/SKILL.md. The author is apify.
Need an agent workflow built around your stack?
Tell us the workflow you want automated. We reply within 4 working hours (IST).
ATTRIBUTION
Verified email finder was written by its original authors and is published in apify/awesome-skills (opens GitHub in a new tab) under the Apache-2.0 license. All rights stay with the original authors.
100xLab did not write, maintain or endorse this skill. This page lists it for discovery. Repo stars belong to the source repository, not to the skill. Review any skill before you install it.