How to Replace a Clay Workflow with Claude + an API
Replace a Clay workflow with Claude and a data API: a step-by-step guide to rebuilding list-building, enrichment, scoring, and outreach in code you own.
Published
Written by
Abhilash Chowdhary
Reviewed by
Manmohit Grewal
Read time
7
minutes

Clay made enrichment a standard part of go-to-market work, and a growing share of the teams who learned it on Clay now want that enrichment as code they can call, not a spreadsheet they have to open. In a 2026 survey of 200 GTM operators, 27% said they had already replaced a GTM tool with Claude and another 30% planned to soon, and many of them were hitting credit walls two to three times a day. If you run a Clay workflow today and have started wishing you could call it from your own code, that is exactly what this guide builds.
The people moving off Clay are usually not anti-Clay. They are Clay power users who hit a programmatic ceiling. One developer-marketing consultant we spoke with put the goal plainly: she wanted to "rip out clay entirely from the stack and build my own agent" that her founder clients could run from a coding agent instead of a spreadsheet UI. This article shows how to do that: rebuild a real Clay workflow, step by step, as a Claude plus data-API pipeline, with copy-paste code for the builder path and a no-code path for everyone else. You can follow along with the $20 in free credits every new Crustdata account starts with.
What a Clay workflow actually is, and why it resists a code rebuild
A Clay table almost always follows the same four steps, whatever the use case.
Build a list: find the companies or people that match an ICP.
Run a waterfall enrichment: pass each record through one provider, then fall back to another when the first comes up empty.
Score or segment: keep the records that pass your criteria and drop the rest.
Push the result: send the enriched, scored list to a CRM or an outreach tool.
That shape is worth naming because it is exactly what you are going to rebuild. An e-commerce outbound agency we interviewed described their version of it in detail, from building a target account list to running a good-fit prompt to a final email-validation waterfall, all assembled inside Clay.
Why not just call Clay from your own code?
You can, up to a point. Clay now ships an API and a CLI, so a backend job or a coding agent can run Clay searches and call Clay functions from code, and Clay's pricing page lists API access on its Enterprise plan. Every call still runs on Clay's meters: actions for the work Clay does, and data credits for any data bought from vendors in its marketplace (Crustdata's own breakdown of why developer teams leave Clay covers this in depth). Re-running a large list every few weeks on those meters is where the cost builds up.
The replacement architecture: Claude plus a data API
The architecture is simpler than it sounds. A Claude Code agent with Crustdata's MCP server configured calls the same Crustdata REST API you could hit directly from Python, so "Claude," "MCP," and "the API" are three layers of one system rather than three competing tools. Claude is the agent runtime, MCP is the protocol it uses to call tools, and the Crustdata API is the data layer underneath.
Each piece of a Clay table maps cleanly onto something you own:
Clay's tables become a store you control, a Google Sheet to start, then a database like Supabase or Postgres as you scale.
Clay's enrichment modules become Crustdata's search and enrichment endpoints, billed per call with no platform markup on top.
Clay's spreadsheet orchestration becomes Claude, or a short script you schedule yourself.
There are two ways to wire this up, and you pick based on who is building. The direct-REST path suits a GTM engineer or technical founder who wants to write the orchestration. The MCP path suits an operator who would rather drive everything through natural language in Claude. Both hit the same data, and you can use Crustdata as the data layer for the internal tools your team builds.
Rebuild the workflow step by step
The worked example below builds a list of high-growth companies, enriches them with a waterfall, scores them, and writes the result to a CRM. Every endpoint is real, and you can run each step on its own.
Step 1: Build the list
This replaces Clay's "find companies" module. Crustdata's Company Search API filters an indexed company database on more than 95 dimensions, so you can express an ICP as a filter object instead of clicking through a UI. Here is a search for US software companies between 50 and 500 people that raised a Series A and grew headcount by more than 20% over the past year, largest first:
curl -X POST 'https://api.crustdata.com/screener/companydb/search' \ --header 'Authorization: Token $auth_token' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "op": "and", "conditions": [ {"filter_type": "hq_country", "type": "=", "value": "USA"}, {"filter_type": "crunchbase_categories", "type": "in", "value": ["Software"]}, {"filter_type": "employee_count_range", "type": "in", "value": ["51-200", "201-500"]}, {"filter_type": "last_funding_round_type", "type": "=", "value": "series_a"} ] }, "sorts": [{"column": "employee_metrics.growth_12m_percent", "order": "desc"}], "limit": 100 }'
curl -X POST 'https://api.crustdata.com/screener/companydb/search' \ --header 'Authorization: Token $auth_token' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "op": "and", "conditions": [ {"filter_type": "hq_country", "type": "=", "value": "USA"}, {"filter_type": "crunchbase_categories", "type": "in", "value": ["Software"]}, {"filter_type": "employee_count_range", "type": "in", "value": ["51-200", "201-500"]}, {"filter_type": "last_funding_round_type", "type": "=", "value": "series_a"} ] }, "sorts": [{"column": "employee_metrics.growth_12m_percent", "order": "desc"}], "limit": 100 }'
curl -X POST 'https://api.crustdata.com/screener/companydb/search' \ --header 'Authorization: Token $auth_token' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "op": "and", "conditions": [ {"filter_type": "hq_country", "type": "=", "value": "USA"}, {"filter_type": "crunchbase_categories", "type": "in", "value": ["Software"]}, {"filter_type": "employee_count_range", "type": "in", "value": ["51-200", "201-500"]}, {"filter_type": "last_funding_round_type", "type": "=", "value": "series_a"} ] }, "sorts": [{"column": "employee_metrics.growth_12m_percent", "order": "desc"}], "limit": 100 }'
The response is a companies array with a next_cursor for pagination, billed at 0.03 credits per result plus 0.2 credits per result for each premium group you filter on, such as funding or headcount. That array is your list. Save the crustdata_company_id and basic_info.primary_domain of each record to your store, because the next step works from those identifiers.
Step 2: Own the full waterfall enrichment
This is the step buyers care about most. One agency founder told us what he actually wanted was to “take clay, remove everything to do with their entire tools, and just have a credit for like calling of different APIs,” and another team simply said they wanted to “own the full waterfall enrichment” rather than route it through a third party. A waterfall in code is just a try-cheap-then-fall-back sequence you control.
Crustdata splits enrichment into a free lookup and a paid profile. Company Identify resolves a domain or a company name to a Crustdata company ID at no cost, and Company Enrich returns the full profile for 2 credits. The waterfall is: try an exact domain match first, fall back to a looser name match only for the misses, and pay for enrichment only once a company is matched.
import requests API = "https://api.crustdata.com" HEADERS = { "Authorization": "Bearer $api_key", "x-api-version": "2025-11-01", "Content-Type": "application/json", } FIELDS = ["basic_info", "headcount", "funding", "web_traffic", "people"] def identify(body): # Free: resolves a domain or a name to a Crustdata company ID r = requests.post(f"{API}/company/identify", headers=HEADERS, json=body) matches = r.json()[0]["matches"] return matches[0]["company_data"]["crustdata_company_id"] if matches else None def enrich(domain, name, company_id=None): # Records from Step 1 already carry their ID and skip the lookups if company_id is None: # Tier 1: exact domain match, free company_id = identify({"domains": [domain], "exact_match": True}) if company_id is None: # Tier 2: looser name match, free, only for the misses company_id = identify({"names": [name]}) if company_id is None: return None # no match anywhere, nothing spent # Tier 3: the full profile, 2 credits, only once a company is matched r = requests.post(f"{API}/company/enrich", headers=HEADERS, json={"crustdata_company_ids": [company_id], "fields": FIELDS}) return r.json()[0]["matches"][0]["company_data"]
import requests API = "https://api.crustdata.com" HEADERS = { "Authorization": "Bearer $api_key", "x-api-version": "2025-11-01", "Content-Type": "application/json", } FIELDS = ["basic_info", "headcount", "funding", "web_traffic", "people"] def identify(body): # Free: resolves a domain or a name to a Crustdata company ID r = requests.post(f"{API}/company/identify", headers=HEADERS, json=body) matches = r.json()[0]["matches"] return matches[0]["company_data"]["crustdata_company_id"] if matches else None def enrich(domain, name, company_id=None): # Records from Step 1 already carry their ID and skip the lookups if company_id is None: # Tier 1: exact domain match, free company_id = identify({"domains": [domain], "exact_match": True}) if company_id is None: # Tier 2: looser name match, free, only for the misses company_id = identify({"names": [name]}) if company_id is None: return None # no match anywhere, nothing spent # Tier 3: the full profile, 2 credits, only once a company is matched r = requests.post(f"{API}/company/enrich", headers=HEADERS, json={"crustdata_company_ids": [company_id], "fields": FIELDS}) return r.json()[0]["matches"][0]["company_data"]
import requests API = "https://api.crustdata.com" HEADERS = { "Authorization": "Bearer $api_key", "x-api-version": "2025-11-01", "Content-Type": "application/json", } FIELDS = ["basic_info", "headcount", "funding", "web_traffic", "people"] def identify(body): # Free: resolves a domain or a name to a Crustdata company ID r = requests.post(f"{API}/company/identify", headers=HEADERS, json=body) matches = r.json()[0]["matches"] return matches[0]["company_data"]["crustdata_company_id"] if matches else None def enrich(domain, name, company_id=None): # Records from Step 1 already carry their ID and skip the lookups if company_id is None: # Tier 1: exact domain match, free company_id = identify({"domains": [domain], "exact_match": True}) if company_id is None: # Tier 2: looser name match, free, only for the misses company_id = identify({"names": [name]}) if company_id is None: return None # no match anywhere, nothing spent # Tier 3: the full profile, 2 credits, only once a company is matched r = requests.post(f"{API}/company/enrich", headers=HEADERS, json={"crustdata_company_ids": [company_id], "fields": FIELDS}) return r.json()[0]["matches"][0]["company_data"]
Because you only pay for enrichment once a company is matched, you decide how close a match has to be instead of the platform deciding it for you. The people section returns the founders and senior leaders at each company, so the same call gives you both the firmographics and the people to reach. Map the response fields you keep to columns in your store:
Crustdata response field | Your store column |
|---|---|
| Company |
| Domain |
| Employees |
| Last round |
| Contact / Title |
| Last refreshed |
Step 3: Score and segment with the agent
This replaces Clay's formula columns and its built-in AI. Scoring can be a simple pass-or-fail check or a natural-language segment. An outbound agency we spoke with scores on three fields and keeps it binary: if the contact is in the right industry and based in the target country, they pass, and everything else is noise. You can run that as plain logic over the enriched records, or hand the list to Claude when the rule is fuzzier, for example "keep the companies whose recent funding and hiring suggest they are scaling a sales team." Because the records are already structured JSON, Claude can segment them into small, targeted lists without another enrichment pass.
Step 4: Push to outreach or CRM, the write-back
A rebuild is only useful if the data lands where your team works, so the last step writes the scored records back. Here is the round-trip closing on a CRM company record:
def push_to_crm(company): payload = {"properties": { "name": company["basic_info"]["name"], "domain": company["basic_info"]["primary_domain"], "numberofemployees": company["headcount"]["total"], "recent_funding_round": company["funding"]["last_round_type"], }} requests.post("https://api.your-crm.com/crm/v3/objects/companies", headers={"Authorization": "Bearer $crm_token"}, json=payload)
def push_to_crm(company): payload = {"properties": { "name": company["basic_info"]["name"], "domain": company["basic_info"]["primary_domain"], "numberofemployees": company["headcount"]["total"], "recent_funding_round": company["funding"]["last_round_type"], }} requests.post("https://api.your-crm.com/crm/v3/objects/companies", headers={"Authorization": "Bearer $crm_token"}, json=payload)
def push_to_crm(company): payload = {"properties": { "name": company["basic_info"]["name"], "domain": company["basic_info"]["primary_domain"], "numberofemployees": company["headcount"]["total"], "recent_funding_round": company["funding"]["last_round_type"], }} requests.post("https://api.your-crm.com/crm/v3/objects/companies", headers={"Authorization": "Bearer $crm_token"}, json=payload)
Swap the endpoint and property names for your CRM, keep the field mapping from Step 2 as your source of truth, and the loop is complete: search, enrich, score, write back, all in code you own.
Send the records to Attio
Attio takes the same write-back with no connector in between. Crustdata has no native Attio integration, so the second call goes straight to Attio's REST API with an API key from your Attio workspace. Attio's upsert endpoint matches on the company's domain, so re-running the job updates the record instead of creating a duplicate.
def push_to_attio(company): payload = {"data": {"values": { "name": [{"value": company["basic_info"]["name"]}], "domains": [{"domain": company["basic_info"]["primary_domain"]}], }}} requests.put("https://api.attio.com/v2/objects/companies/records", params={"matching_attribute": "domains"}, headers={"Authorization": "Bearer $attio_token"}, json=payload)
def push_to_attio(company): payload = {"data": {"values": { "name": [{"value": company["basic_info"]["name"]}], "domains": [{"domain": company["basic_info"]["primary_domain"]}], }}} requests.put("https://api.attio.com/v2/objects/companies/records", params={"matching_attribute": "domains"}, headers={"Authorization": "Bearer $attio_token"}, json=payload)
def push_to_attio(company): payload = {"data": {"values": { "name": [{"value": company["basic_info"]["name"]}], "domains": [{"domain": company["basic_info"]["primary_domain"]}], }}} requests.put("https://api.attio.com/v2/objects/companies/records", params={"matching_attribute": "domains"}, headers={"Authorization": "Bearer $attio_token"}, json=payload)
Add custom attributes in Attio for headcount and last round, then write them in the same call.
Re-run it on a schedule without the credit wall
The reason this is worth building is what happens next month. A GTM engineer at a martech company told us the hardest part of his Clay setup was that "Clay is quite expensive and also it's very difficult to basically re-enrich every couple of weeks or every quarter." Once the workflow is a script, re-running it is a scheduled job:
# crontab: re-enrich the stored list at 6am on the first of each month 0 6 1 * * /usr/bin/python3 /opt/gtm/enrich_pipeline.py
# crontab: re-enrich the stored list at 6am on the first of each month 0 6 1 * * /usr/bin/python3 /opt/gtm/enrich_pipeline.py
# crontab: re-enrich the stored list at 6am on the first of each month 0 6 1 * * /usr/bin/python3 /opt/gtm/enrich_pipeline.py
For change-driven updates rather than calendar-driven ones, the Watcher API pushes a webhook when something you track actually moves, such as a funding round, a headcount jump, or a leadership change. That turns re-enrichment from a polling loop you pay for repeatedly into an event you react to, which is the same shift from batch refresh to real-time signals that pulled these teams off Clay in the first place.
What does Claude plus Crustdata cost compared with Clay?
For a team rebuilding a Clay workflow as code, Crustdata is the data layer that pairs with Claude: published per-call pricing for company and person data, and a hosted MCP server Claude signs into with OAuth. Clay prices by plan instead. In October 2026, Clay's pricing page listed the Launch plan from $185 a month with 2,500 data credits and 15,000 actions, and the Growth plan from $495 a month with 6,000 data credits and 40,000 actions. The Crustdata half comes down to math on your own list. Searching 1,000 companies costs 30 credits at 0.03 credits per result, and filtering on funding or headcount, as Step 1 does, adds 0.2 credits per result for each. Enriching the 100 companies that pass your score costs 200 credits more, at 2 credits per company. Claude's usage runs on the Claude plan or API account you already pay for. Every Crustdata response carries an `X-Credits-Used` header, so you can log what each run spends and set it against what the same table spends in Clay. New accounts start with $20 in free credits, so you can price one workflow before you buy.
For the wider comparison on data coverage and freshness, see Crustdata vs Clay.
No engineer? Run the whole thing through Claude's MCP
You do not have to write any of the code above. A four-person fractional sales team we interviewed runs the entire workflow through Claude, which one of them called "the operating system for our little business." The setup is short:
Install the Crustdata MCP server by adding `https://install.crustdata.com/mcp` as a custom connector in your Claude client, then sign in with your Crustdata account.
Describe the workflow in plain language: ask Claude to find companies that match your ICP, enrich them, and keep the ones that fit.
Turn the repeated request into a skill so the same campaign runs from one instruction next time, the way that team "skillified" their sub-500-lead campaigns.
Send the results to a Google Sheet to start, and move to a database once the workflow earns its keep.
The same person told us why this beat their old setup: "I just want a tool I can plug into the back of Claude and get good data at scale." The MCP path gives the operator the outcome and the builder path gives the engineer the control, and both read from the same data layer.
When to keep Clay
Moving to code is not the right call for every workflow, and it helps to be honest about that. Clay's biggest strength is that every step is visible and fixable: when a client-facing run breaks, you can open the table, find the column that errored, and repair that one module. An agentic workflow can be harder to audit, and as one GTM engineering team put it, the logic can feel like a black box where "you send a prompt, it reasons in the background, and it returns an output". For high-value, client-facing runs that need row-level review before anything syncs, that auditability is worth keeping. One buyer was candid that he could not fully leave because he runs "a lot of orchestration inside of clay."
The way to get the control without the black-box risk is to write deterministic steps and keep a human review before the write-back, which is what these teams already do. Crustdata is the data layer for teams building programmatic outbound and internal tools they want to own end to end, and a Clay table stays useful for hands-on client runs that need row-level review.
Conclusion
Replacing a Clay workflow with Claude and an API comes down to four moves you now control: build the list, run your own waterfall, score the records, and write them back, then schedule the whole thing or trigger it from a signal. The teams making this switch want a data layer they own, priced by the call, that an agent can drive.
Start with one workflow. Rebuild your most expensive Clay table as a script or a Claude skill, point it at a real data API, and run it once end to end. If it holds up, schedule it and move the next one. You can sign up and get $20 in free Crustdata credits to test the enrichment calls, or book a demo to walk through the architecture for your stack.k.
Products
Popular Use Cases
Competitor Comparisons
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 Crustdata Inc.
Products
Popular Use Cases
Competitor Comparisons
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 CrustData Inc.
Products
Popular Use Cases
Competitor Comparisons
95 Third Street, 2nd Floor, San Francisco,
California 94103, United States of America
© 2026 Crustdata Inc.
🎉 We hit $10M ARR!
$600k to $10m in 1.5 years. Our entire GTM is powered by our own APIs.
🎉 We hit $10M ARR in 1.5 years!



