Install
$ agentstack add skill-jimmy-creatop-apollo-operator-apollo-list-builder ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README — it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Apollo List Builder (List)
Take the validated ICP from profile.yaml and turn it into a clean, enriched, graded lead list that is ready for a sequence. Search, enrich, apply the scoring signals, dedupe, then grade. Nothing goes to a sequence until it passes the scorecard.
When to use
- Targeting is done:
profile.yamlhas a validated ICP, the Apollo filter set, and 1 to 3 scoring signals. - You need the full list, not the 50-to-100 sample you already validated.
If profile.yaml has no validated ICP yet, stop and run Targeting (apollo-icp-builder) first. Building a full list on an unvalidated ICP is the most expensive mistake in outbound.
The order matters: filter and dedupe before you enrich
Enrichment costs credits (1 per person requested, matched or not). People search costs nothing, and company search costs 1 credit per call. So the pipeline spends as little as possible: narrow hard in search, filter to verified, dedupe, suppress, then enrich only the people you actually want. Never enrich first and filter later.
Where the list lives (files on your drive)
Important: the list is not "in the model." Apollo's search returns records into the conversation, but you persist them to real files on the local drive, one per stage. This is so the list survives (you cannot hold thousands of rows in context), and so you can open any stage in a spreadsheet and see exactly what is happening. Convention:
lists//
people_p*.json # step 1: raw search pages, straight from the CLI
people_all.json # step 1: the pages merged, then deduped by id
companies_all.json # step 2: company universe, for the NAICS composition check
deduped.csv # step 3: after dedupe, suppression, and per-company cap
enriched.json # step 5: FULL enrichment payload, every field
enriched.csv # step 5: the columns you actually want, generated from enriched.json
scored.csv # step 6: + scoring-signal tag and priority
verified.csv # step 7: after the independent verifier pass
final.csv # step 8: passed the scorecard, ready for a sequence
Each step reads the previous file and writes the next. Nothing lives only in the model, and you can inspect the work at any point. Raw people-search pages are free to regenerate, so they are the one thing you can safely delete; enriched.json is the opposite, because it is what the credits bought and it lives nowhere else.
Route results to disk, never into the model. On the CLI this is free and automatic: > file.json and the payload never touches context. On MCP it is a constraint you have to work around, because a 100-record search and even a 10-record enrichment each return a payload too big for the window, and the harness parks it in a file. Either way, process the file, do not read it into the conversation.
When you enrich, preserve the full payload. Apollo returns roughly 33 fields per person (emails, phones, LinkedIn, employment history, seniority, intent). Save the complete JSON (enriched.json), then generate whatever columns you want as a CSV view from it (enriched.csv), and let the user pick which to keep or drop. Never reduce to a handful of columns and throw the rest away: you paid credits for that data, and it does not live anywhere else unless you save it.
Pipeline
1. Build the raw list (search)
Paginate 100 per page, up to Apollo's 50,000-record ceiling (500 pages). Collect each person's id. Search returns no emails and may mask last names, that is expected and fine, enrichment fixes it.
Resolve company ids with the free tool. apollo_organizations_lookup does fuzzy name lookup and returns shallow records (id, name, domain, website) at no credit cost. Use it whenever you only need to turn a name into an id. The paid company search is for when you genuinely need full firmographics back.
People search is free. Company search is not. Measured: people search left lead_credit.consumed unchanged across dozens of pages, while companies search cost 1 credit per call (a 21-page company pull cost 21 credits). So paginate people as wide as you like, but treat company pulls as a real, if cheap, spend.
Narrow to verified emails at search time, not after enriching. --email-status verified (CLI) / contact_email_status: ["verified"] (MCP) filters to people Apollo already believes have a good address, and it costs nothing. In a real run this cut a 3,240-person universe to 2,742, which meant not paying to enrich ~500 people whose emails would have been thrown away anyway. Apply it in the search, and you never spend the credit in the first place.
Pick your lane first (see apollo-operator):
- CLI, when the filters are expressible as CLI flags and the list runs to hundreds or thousands. Results redirect to disk and never enter the model's context. A real run pulled 3,238 people into 4 MB on disk with nothing in the window, which simply does not fit over MCP.
- MCP (
apollo_mixed_people_api_search), when the ICP needs a filter the CLI does not have: NAICS, SIC, founded year, headcount growth, tenure, years of experience, market segments, or department headcounts. The CLI is not a superset.
The CLI loop, with the exact people_search_filters from profile.yaml:
for p in $(seq 1 ); do
apollo people search --per-page 100 --page $p -f json > lists//people_p$p.json
[ "$(jq '.people|length' lists//people_p$p.json)" = "0" ] && break
sleep 0.4
done
jq -s '[.[].people[]]' lists//people_p*.json > lists//people_all.json
Never -f csv here: it dumps the whole array into one cell. Shape with jq … | @csv instead. Recipes: cli-recipes.md.
If the count is far above what you need, tighten filters rather than enriching a bloated list.
Never trust that a filter returned what you asked for
This is the single most repeated lesson in this skill, and it shows up in three different disguises. Run all three checks the moment the pages are merged, before a credit is spent. They cost nothing.
1. Titles. Apollo's title matching is looser than it looks, and the failure is quiet: a search for founder also returns Founding Engineer, Founding Designer, Founding GTM, Founding Recruiter, and Founding Member. Those people are not the buyer and will not become the buyer. In real runs this was 9 to 14% of the list. Nobody notices until a founding engineer replies asking why you are pitching them.
jq -r '.[].title' people_all.json | sort | uniq -c | sort -rn | head -40
Read that with your ICP in hand and write an explicit persona filter: an allow-pattern for the titles you want, plus a deny-pattern for the near-misses. The same trap exists for seniority, where c_suite sweeps in Chief Business Officer, Chief People Officer, and similar roles that may be irrelevant to your offer.
2. Duplicates. Pagination overlaps (see below). Compare row count to unique-id count.
3. Sector. Grade the NAICS mix (step 2). A keyword tag like b2b saas will pull in adjacent industries, some legitimate and some not.
The underlying rule: a filter expresses your intent, it does not guarantee the result. Print the distribution of anything you filtered on and read it before you pay for it.
2. Grade the composition before you spend anything
industry comes back null in search results on both lanes, so the obvious ICP-fit check is not available pre-enrichment. naics_codes and sic_codes are populated, and they are enough.
Pull the matching company universe (also free), join it to the people list on company name, and read the sector mix. That tells you how much of the list is genuinely your ICP before a single credit is spent. The full jq join is in cli-recipes.md.
In a real run this turned 3,238 raw people into a readable mix: 43% professional and technical services, 19% software and information, then a tail of finance, manufacturing, education, health care, and real estate. Education and health care were drift for a B2B offer and got cut; the core 60% became the High tier. Name-based joining matched 88%, so treat the unmatched remainder as its own bucket rather than dropping it.
This is the cheap version of the Targeting sample-validation gate, run over the whole list instead of a sample.
3. Dedupe, suppress, and cap concentration (before enriching)
Three separate jobs, all of which must happen before you pay for enrichment.
Dedupe by person id, first, before anything else. This is not optional and it is not obvious: Apollo's pagination returns overlapping records across pages. A real 33-page pull returned 3,238 rows containing only 2,933 unique people, a 9% duplicate rate. If you dedupe by company or apply a per-company cap before deduping by id, the duplicates survive into the final list and you pay a credit for each one.
jq 'unique_by(.id)' people_all.json > deduped_ids.json
Do this the moment the pages are merged. Then check it: jq 'length' and jq '[.[].id]|unique|length' should be the same number. If they are not, you have not deduped.
Suppress against your other lists, not just this one. If you are running more than one angle against the same ICP, the angles overlap, and the overlap is invisible unless you check for it. In a real run of three angles against one ICP: 85 people in the second list had already been enriched for the first, and 19 more appeared in both of the new lists. Every one would have been paid for twice and emailed twice.
Keep a per-business suppression file of every person id you have already enriched or enrolled, and subtract it from each new list before enriching. When two live angles both want the same person, assign them to the higher-intent one and remove them from the other, deliberately rather than by accident.
comm -12 new_ids.txt already_enriched.txt | wc -l # what the overlap costs you
Suppress against people you must never email. Deduping the list against itself is not enough. There are people who fit the ICP perfectly and must still be excluded, and every one of them is a credit wasted and a relationship damaged:
- Existing customers, and anyone at a customer account.
- Open deals and active opportunities.
- Anyone who has ever unsubscribed, replied "not interested", or complained.
- Partners, vendors, investors, and your own employees.
- Anyone already sitting in another live sequence.
The brief's Constraints section is where these live (see business-brief). Build the suppression list as a file of domains and email addresses, and subtract it from deduped.csv before enriching. If the business cannot tell you who is on it, that is a finding worth surfacing, not a step to skip.
Apollo enforces a second layer at enrollment, and it is opt-in. sequences add-contacts blocks these by default, and each is a flag you have to deliberately turn on: --active-in-other, --finished-in-other, --same-company, --unverified-email, --no-email, --skip-verification. Leave them off. A clean list still produces a bounce spike if somebody passes --skip-verification at enrollment. Your file-level suppression and Apollo's flags are complementary, not redundant: yours catches customers and unsubscribes, Apollo's catches double-touching.
Cap concentration. No more than 2 to 3 people per company unless it is deliberate ABM. One company flooding your list skews everything and looks like spam.
4. STOP. Show the operator what they are looking at, and wait.
This is a required gate, not a courtesy. Everything up to here is free and unenriched, which means the list you are about to show has no email addresses, no phone numbers, obfuscated last names, and no company industry or headcount. Those fields are blank because nobody has paid for them yet, not because anything went wrong.
You know that. The person you are working with very likely does not. If you hand over a CSV full of empty columns without saying this, the reasonable conclusion is that the tool is broken, and the next thing that happens is either a lost user or a re-run that wastes credits.
So say it plainly, in this shape:
> Here is the list at the free stage: 1,759 people across 1,245 companies. Nothing has been enriched, so there are no emails yet and last names are masked. That is expected at this point. > > What I can tell you now: 999 are High priority (IT services, software, consulting), 508 Medium, 252 Low. Companies are capped at 2 people each, and I cut education, health care, and real estate as a poor fit for your offer. > > Please review the tiering and the cuts before I spend anything. Once you are happy, enriching the 999 High-priority leads costs about 999 credits and returns emails, phones, and roughly 33 fields per person. You have 3,900 credits.
Three rules for this gate:
- Name the missing columns out loud. Do not rely on the operator noticing, and do not rely on them knowing why.
- Give them something reviewable. Tier counts, what you excluded and why, and the per-company cap. "Here is a CSV" is not a review.
- State the credit cost of the next step before taking it, then wait for an actual yes.
The whole point of separating the free stage from the paid stage is that the operator gets to change their mind while it is still free. That only works if they understand what they are looking at.
5. Enrich (the credit step)
Before you start, confirm the total scope out loud with the exact wording the tool requires: "This will enrich [N] people and consume up to [N] credits (1 credit per match, no charge for unmatched). Do you want to proceed?" Confirm the whole scope up front, do not drip-confirm batch by batch.
The 85% rule: never quietly spend most of what they have left.
Before enriching, compare the cost against the remaining balance, not the monthly limit. If the step would consume more than 85% of remaining credits, stop and put the choice in front of the operator with three explicit options:
> Enriching this list costs 2,712 credits. You have 3,055 left, so this would use 89% of them and leave you with 343. > > 1. Downsize — enrich the top N and keep the rest staged for later. The list costs nothing to hold. > 2. Skip — leave the list at the free stage and come back when credits reset. > 3. Proceed — spend it, knowing what is left. > > Which would you like?
This is not the same as asking permission to spend credits, which you already do. It is a separate, louder gate for a spend that materially changes what they can do next. An operator who has run outbound before will know 343 credits is nearly nothing. Someone doing this for the first time will not, and "you have 343 credits left" only means something once it is too late.
Do not compute this against the plan limit. 2,712 of a 4,000 limit sounds fine; 2,712 of 3,055 remaining does not, and the second number is the real one.
Enrich as late as you can. Enriched data has a shelf life: people change jobs, and a verified address goes stale. If sending is weeks away (a warmup clock still running, for instance), build and grade the list now and enrich when you are close to actually sending. The list costs nothing to hold; the enrichment does.
Pick the method by size.
- Under ~20 people, in conversation:
apollo_people_bulk_matchon MCP, passing theidfrom search (never the names). Max 10 per call, 1 credit per match. - Anything larger: use the CLI.
apollo people bulk-enrich --filetakes a JSON array of match records, and{"id": ""}is a valid record. Split the id list into batches of 10, loop, and write each response to disk. Live-tested at 848 people across 85 batches: zero failures, exactly 848 credits, 33 fields per person, about 100 seconds. The payloads never touch context.
split -l 10 enrich_ids.txt batches/batch_
for b in batches/bat
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [jimmy-creatop](https://github.com/jimmy-creatop)
- **Source:** [jimmy-creatop/apollo-operator](https://github.com/jimmy-creatop/apollo-operator)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.