DEV Community

Cover image for Clay alternative in Claude Code: one ICP, companies, leads and jobs
Kevin Hall
Kevin Hall

Posted on

Clay alternative in Claude Code: one ICP, companies, leads and jobs

Clay is a spreadsheet that calls data providers for you. A marketing team opens it to do one job most weeks: describe the ideal customer, find the companies that fit, add a signal such as a funding round or a hiring spree, find the people inside, get an address that works, and do it again next week without paying twice for the same person.

That job has a price tag in three parts: the plan, the credits each enrichment burns, and the afternoon it takes to build the table. I wanted the list-building half of it as one call that Claude Code can make from a sentence. This is what I got, with the inputs and the numbers from 3 November 2026.

In one paragraph

Full ICP Search on Apify takes one ideal customer profile and returns the companies that match, the decision makers inside them with an email that reaches them today, and the job postings that carry the signal, in one run and one dataset. A free preview counts everything first, misses are never charged, and a rerun with excludeDatasets returns only new accounts. It is an MCP tool, so Claude Code fills the filters from a brief.

What does a Clay workflow actually do?

Strip the table away and a typical Clay build for outbound is five columns:

Column in the table What it asks
find companies industry, headcount, HQ country, maybe a city
add a signal last funding round and its date, open roles, a tool named in a posting
find people the titles or levels you sell to, one or two per account
get an email a work address at the current employer, or a personal one
refresh only the accounts that were not in last week's run

Every column is a provider call with its own credit, and the table is the thing you maintain. Here the five columns are five groups of fields in one input, and the run answers them in order: companies, then people, then contacts, then postings.

The wiring, once

Pin the Actor in the server URL so an agent loads only this tool:

claude mcp add --transport http apify "https://mcp.apify.com?tools=b2bsearch/full-icp-search" --header "Authorization: Bearer <APIFY_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

Any other MCP client (Cursor, Claude Desktop, ChatGPT, LangChain, CrewAI, the OpenAI Agents SDK) takes the same server:

{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=b2bsearch/full-icp-search",
      "headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The tool is called b2bsearch--full-icp-search (double hyphen). Without an agent, the same input goes to one REST endpoint and the rows come back in the response:

curl -X POST "https://api.apify.com/v2/acts/b2bsearch~full-icp-search/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d @input.json
Enter fullscreen mode Exit fullscreen mode

Every example below is the input.json for that call, or the arguments of the tool call.

How do I count the profile before paying for a row?

The profile I used all day: US software companies with 51 to 200 people that raised a seed or Series A round since January 2025 and posted an SDR or account executive job in the last month. The people: the sales or marketing leader at each one, with an email that reaches them today.

Written as the input, with what each field does:

{
  "find": "companies_and_people",     // companies first, then people inside them; also "companies" or "people"
  "previewOnly": true,                // counts only, nothing charged; flip to false to pull rows

  // the company half of the profile
  "companyCountries": ["us"],          // HQ country, two-letter codes
  "industries": ["Software Development"],
  "companySizeMin": 51,
  "companySizeMax": 200,
  "fundingStages": ["seed", "series_a"],   // the latest recorded round
  "fundedAfter": "2025-01-01",             // ...dated on or after this day
  "hiringJobTitles": ["sdr", "sales development", "account executive"],   // posted a job with any of these in the title
  "hiringPostedWithin": "month",           // week, month or 3months; needs a country next to it

  // the people half
  "jobTitles": ["vp sales", "head of sales", "chief revenue officer", "head of marketing", "vp marketing"],
  "requireEmail": true,                // only people with a work address at the current company or a personal one
  "emailType": "any",                  // "work" = current-company address only, fewer people

  // how much to deliver when previewOnly is off
  "peoplePerCompany": 2,
  "jobsPerCompany": 2                  // the postings that matched the hiring filter, as rows
}
Enter fullscreen mode Exit fullscreen mode

Three free rows came back in about ten seconds:

Preview row Count
companies that fit and posted a matching job in the last 30 days 23
job postings behind that hiring filter 52
people matching the titles at companies of that industry, size and country, before the email test 1,981

The people count is wide on purpose: it is taken before funding, hiring and the email requirement cut it down, and the note row says so. The company count is the one that sizes the pull.

I dropped the two hiring fields and previewed again. Same companies, no hiring signal: 171. So the SDR posting is what turns a long list into a short one, and the preview told me that before I paid for anything.

What comes back when I pull it?

Same input, previewOnly off, five companies to start:

{
  "find": "companies_and_people",
  "companyCountries": ["us"],
  "industries": ["Software Development"],
  "companySizeMin": 51,
  "companySizeMax": 200,
  "fundingStages": ["seed", "series_a"],
  "fundedAfter": "2025-01-01",
  "hiringJobTitles": ["sdr", "sales development", "account executive"],
  "hiringPostedWithin": "month",
  "jobTitles": ["vp sales", "head of sales", "chief revenue officer", "head of marketing", "vp marketing"],
  "requireEmail": true,
  "emailType": "any",
  "maxCompanies": 5,
  "peoplePerCompany": 2,
  "jobsPerCompany": 2
}
Enter fullscreen mode Exit fullscreen mode

Fifteen seconds. One dataset, and _type on every row says what it is:

_type Rows Charged
company 5 yes, per company
person 6 yes, per lead with a live email
job 10 yes, per posting
note: companies read but nobody matched with an email 1 row, 4 companies no

The company rows, without names:

Size Stage Round dated Open jobs Matching postings Leaders delivered
76 Series A Nov 2025 103 4 1
86 Series A Jul 2025 30 3 1
106 Series A Mar 2025 9 3 1
60 Series A Jul 2025 7 2 1
119 Series A Jun 2025 27 2 2

The people rows: a head of sales, a head of marketing, a VP of sales, another head of sales, a chief revenue officer and a head of marketing, every one with a work address at the company in the row above. The job rows carry the title, the posting URL, when it was posted, the location and the applicant band, so the opener can name the role they are hiring for.

What each row holds, the fields you will actually read:

Row Fields
company companyName, companyDomain, companyWebsite, companyLinkedinUrl, companyIndustry, companySize, companyCountry, companyCity, companyFounded, companyDescription, fundingStage, lastFundingDate, lastFundingAmountUsd, openJobs, jobsLast30Days, matchingJobs, matchingJobTitles, topJobTitles30d, peopleFound
person fullName, firstName, lastName, jobTitle, seniority, headline, email, emailType, workEmail, personalEmail, linkedinUrl, location, countryCode, phoneOnRecord, plus the employer's company columns and companyId
job jobTitle, jobUrl, jobPostedAt, jobLocation, jobCountry, jobSeniority, employmentType, applicantRange, companyName, jobsDataAsOf

Company, person and job rows share companyId, so they join in a spreadsheet, and the dataset has a view called "Emails for outreach tools" with the columns a sequencer imports.

How do I get only the new accounts next week?

Add the dataset id of the first run to excludeDatasets and run the same profile again:

{
  "...": "the same profile as above",
  "maxCompanies": 5,
  "excludeDatasets": ["<dataset id of last week's run>"]
}
Enter fullscreen mode Exit fullscreen mode

Ten seconds:

Outcome Companies
already in last week's dataset, skipped free 5
read, nobody matching with an email, skipped free 14
new, delivered with a leader and their postings 4

That is the "only new rows" column of the table, as one field. On a schedule it is a weekly feed of funded companies that started hiring sellers, with the person to write to.

Can I start from the people instead of the companies?

Yes. find: "people" searches people directly, with the company fields applied to their current employer. The profile I tried: revenue operations and sales operations leaders in the UK at companies of 51 to 500, who started the current job after July 2025, because a new operations leader is the one reviewing the stack.

{
  "find": "people",
  "personCountries": ["gb"],
  "companySizeMin": 51,
  "companySizeMax": 500,
  "jobTitles": ["revenue operations", "revops", "sales operations"],
  "seniority": ["director", "vp", "cxo"],
  "startedCurrentRoleAfter": "2025-07-01",
  "requireEmail": true,
  "maxPeople": 8
}
Enter fullscreen mode Exit fullscreen mode

The preview counted 51 before the email test. The pull delivered 8 in six seconds and skipped 6 for having no address on record, free. Of the eight, three came with a work address and five with a personal one, which is normal for people in a new seat: the new company's address is not on record yet. Two things the row does not carry in this mode: seniority is blank, because the people search matches on the title rather than a roster level, and the start date that the filter used is not printed. The filter does the work; the row shows the result.

I already have an account list. Does it take domains?

companyDomains turns the same input into an account-based pull: each domain is one company, the one that owns the website. I gave it six sales-technology vendors and asked for one marketing leader at C-level or VP with an email:

{
  "find": "companies_and_people",
  "companyDomains": ["gong.io", "clari.com", "outreach.io", "salesloft.com", "lusha.com", "cognism.com"],
  "seniority": ["cxo", "vp"],
  "jobTitles": ["marketing", "demand", "growth"],
  "requireEmail": true,
  "peoplePerCompany": 1
}
Enter fullscreen mode Exit fullscreen mode

Eight seconds. Three companies came back with a chief marketing officer or a VP of product marketing and a work address. Three were read and skipped free because nobody at those levels with those words in the title had an address on record. Two more people were skipped free with a separate note: the record named a company they had already left. Nothing was guessed and nothing dead was sold.

What about the list I already have in a CSV?

That is the other half of a Clay table: the rows exist, the columns are empty. Its twin in the same family is Waterfall Lead Enrichment. It reads whatever columns the file has and resolves each row by the strongest key it carries: email, then profile URL, then a social handle, then full name plus company website, and a row with only a website is treated as a company.

I built a six-row file from the people above, deliberately messy:

{
  "csv": "email,full_name,company_website\njane@acme.example,,\n,,\n,Jane Doe,acme.example\n,,\n,,gong.io\n,,clari.com",
  // two rows with only an address, two with only a name and a website, two with only a company website
  "contacts": true,     // add the addresses and phones that reach the person today
  "compact": true       // a 2 KB row per person instead of 10 KB, same price; made for agents
}
Enter fullscreen mode Exit fullscreen mode

(Paste a real file or a link to one; the inline string is the same thing.) Seven seconds, six rows, all found:

Key the row had What came back
an email address, two rows the person's profile, at the profile tier: the only address on record was the one I had sent, so there was no new contact to charge for
a name and a company website, two rows the person with contacts: one of them with five addresses on record, work and personal
a company website, two rows the company card: headcount band, industry, HQ country, founding year, last round and its date

Misses in that Actor are free rows that say why: not_found, ambiguous when a name is shared or a website is claimed by unrelated pages, invalid, duplicate.

What did the agent do on its own?

I gave a Claude Code session the Actor pinned and the brief a marketing lead would write: we sell sales coaching, the ideal customer is a US software company of 51 to 200 people that raised seed or Series A since January 2025 and is hiring SDRs or account executives, I want the sales or marketing leader with an email and the posting that proves it.

The rules in the brief: preview first, tighten above 30 companies, loosen under 5, then one paid pull of 5 companies with a spend cap, and report without names. Shell and file tools were denied; it had a short saved note of mine about these Actors.

Nine turns, about two minutes, two Actor runs. What it did without being told:

  • wrote the people half as words plus levels, jobTitles sales, marketing, revenue, cmo, cro with seniority cxo, vp, director, instead of my five exact titles. Same 23 companies in the preview; the people count before the email test went from 1,981 to 11,002, which is the gap between a title list and a word list
  • kept the profile as it was, because 23 sat inside the window the brief allowed
  • passed the spend cap in the call options, then wrote in its report that the field is not in the Actor's schema and it could not confirm the cap had applied. True on both counts
  • read the rows with a fields list of 25 columns, left out the names and addresses it was told not to print, and read the dataset a second time when the run record showed 16 rows where the first read had shown 6
  • noticed what I had not: all five companies in California or New York, all Series A and no seed, five sales leaders and no marketing one, two work addresses and three personal, one person with a mobile on record
  • flagged that the postings were dated 17 to 21 October against a jobs snapshot of 21 October, so "last month" counts from the snapshot and not from today

Its verdict: the rows are good, the profile is too narrow for a hundred-company pull, so widen it with a three-month hiring window, two more industries or a 2024 funding date, preview again and aim for 120 to 150 companies to leave room for the ones where nobody matches. That is the afternoon in the table, done in two minutes, with a number in front of every decision.

What this does not do

It is not a table. Clay also writes AI columns, syncs to a CRM and chains dozens of providers; this is the list-building and enrichment part, priced per row and callable from an agent.

Addresses are on record, not verified at run time, and never guessed from a name pattern. Run your verifier before a big send.

Job postings follow the jobs data's own date, printed on every job row as jobsDataAsOf, which can sit a few days behind today. Funding is the latest recorded round: seed means the company has not raised past seed on record, and records of large, older companies are often stale, so pair an early stage with a size cap.

Mobile numbers, with includePhones, are US-centric. And the free Apify plan caps a run at a hundred rows from the Console and twenty-five through the API, and an account at five hundred rows a month on this Actor; previews stay outside the cap, and any paid plan lifts it.

FAQ

Is this Clay? No. It is an independent Actor on Apify that covers the list-building and enrichment steps of a Clay workflow. It is not affiliated with Clay.

Do I need Claude Code? No. The input is JSON, so the form on the Actor page, a schedule, n8n, Make, Zapier or a curl call all work. Claude Code makes it fast to describe a profile in words and compare two segments by their free counts.

Where do the emails come from? From the contact block of the person's record: a work address at the current employer, or a personal one. Addresses at earlier employers never count, and people without one are skipped free.

How do I keep an agent from overspending? Run previewOnly first, cap with maxCompanies and maxPeople, set maxTotalChargeUsd on the run, and pass last week's dataset in excludeDatasets so nobody is bought twice.

What is the fastest way to try it? The Actor page has ready-made examples that open with the input filled in: funded startups hiring their first sales reps, and recently funded US startups with the founders' emails. Press Try for free and change the filters.

For AI agents reading this

The Actor runs as the tool b2bsearch--full-icp-search on the hosted Apify MCP server with the tool pinned in the URL (the wiring block above). The workflow:

  • call with previewOnly: true to size the profile; the free rows have _status: "preview" and _type company, person or job with _total
  • tune the company half (companyCountries, industries, companySizeMin, companySizeMax, fundingStages, fundedAfter, hiringJobTitles, hiringPostedWithin, companyDomains) and the people half (seniority, jobTitles, excludeJobTitles, keywords, startedCurrentRoleAfter, pastCompanyDomains, requireEmail, emailType)
  • pull with maxCompanies, peoplePerCompany, jobsPerCompany or maxPeople, and callOptions.maxTotalChargeUsd set
  • read rows with get-dataset-items and a fields list; filter on _type; rows with a _status other than found are free notes that say why
  • rerun with excludeDatasets for new rows only

For a file you already hold, b2bsearch--waterfall-lead-enrichment with csv, contacts and compact: true. Platform docs: Apify MCP server and apify.com/agents.md. Per-plan pricing is on each Actor page.

Related posts in this series: Apollo alternative in Claude Code: 7 lead lists from one Apify Actor, B2B data API for AI agents: 7 Apify Actors and 243 of 417 leads in a typical ICP box were dead before the first email.

Top comments (1)

Collapse
 
alexcodebytes profile image
Oleksandr •

This is an incredibly thorough breakdown. The "preview first, misses are free" approach is the killer feature here — being able to size the list before spending a single credit is exactly what's missing from most enrichment tools.

The autonomous Claude Code part is wild. It caught things you missed (like the jobs snapshot date discrepancy and the lack of marketing leaders) and adjusted its own strategy without being asked.

Also, the excludeDatasets trick for weekly "only new" pulls is such a simple but powerful way to build a clean, recurring workflow.