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

May 29, 2026

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 on Crustdata's free tier, which includes 100 credits.

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 you cannot just call Clay from your own code

The blocker is structural. Clay does not expose a public request-and-response API you can call from an application, so you cannot trigger a Clay enrichment from your backend and get the result back in the same call. Its HTTP features run inside the platform, and integrations move through webhooks rather than a REST endpoint you control (Crustdata's own breakdown of why developer teams leave Clay covers this in depth). The practical result is an export-and-import loop: data lives in Clay's tables, and getting it into your own pipeline means moving CSVs by hand. Add a credit model where a single lookup can cost several credits, and re-running a large list every few weeks becomes the expensive part. The fix is to put the data layer under your own code.

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 a live 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, sorted by headcount growth:

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 JSON array of companies with a next_cursor for pagination, billed at one credit per 100 results. That array is your list. Save the company_id and company_website_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 gives you two enrichment modes on one endpoint. Database enrichment returns a cached profile for one credit, and real-time enrichment fetches a fresh profile for five credits when a company is not already in the database. The waterfall is: try the database first, fall back to real-time only for the misses.

import requests

HEADERS = {"Authorization": "Token $auth_token"}
BASE = "https://api.crustdata.com/screener/company"
FIELDS = "headcount,funding_and_investment,web_traffic,decision_makers"

def enrich(domain):
    # Tier 1: database enrichment, 1 credit
    r = requests.get(BASE, headers=HEADERS,
                     params={"company_domain": domain, "fields": FIELDS})
    data = r.json()
    if data and data[0].get("headcount"):
        return data[0]
    # Tier 2: real-time fallback, 5 credits, only for the misses
    r = requests.get(BASE, headers=HEADERS,
                     params={"company_domain": domain, "fields": FIELDS,
                             "enrich_realtime": "true"})
    return r.json()[0]
import requests

HEADERS = {"Authorization": "Token $auth_token"}
BASE = "https://api.crustdata.com/screener/company"
FIELDS = "headcount,funding_and_investment,web_traffic,decision_makers"

def enrich(domain):
    # Tier 1: database enrichment, 1 credit
    r = requests.get(BASE, headers=HEADERS,
                     params={"company_domain": domain, "fields": FIELDS})
    data = r.json()
    if data and data[0].get("headcount"):
        return data[0]
    # Tier 2: real-time fallback, 5 credits, only for the misses
    r = requests.get(BASE, headers=HEADERS,
                     params={"company_domain": domain, "fields": FIELDS,
                             "enrich_realtime": "true"})
    return r.json()[0]
import requests

HEADERS = {"Authorization": "Token $auth_token"}
BASE = "https://api.crustdata.com/screener/company"
FIELDS = "headcount,funding_and_investment,web_traffic,decision_makers"

def enrich(domain):
    # Tier 1: database enrichment, 1 credit
    r = requests.get(BASE, headers=HEADERS,
                     params={"company_domain": domain, "fields": FIELDS})
    data = r.json()
    if data and data[0].get("headcount"):
        return data[0]
    # Tier 2: real-time fallback, 5 credits, only for the misses
    r = requests.get(BASE, headers=HEADERS,
                     params={"company_domain": domain, "fields": FIELDS,
                             "enrich_realtime": "true"})
    return r.json()[0]

Because you only escalate to the more expensive call on a miss, you decide the cost-versus-freshness trade-off instead of the platform deciding it for you. The decision_makers field 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_name

Company

company_website_domain

Domain

headcount.latest_count

Employees

funding_and_investment.last_round_type

Last round

decision_makers[].name / .title

Contact / Title

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["company_name"],
        "domain": company["company_website_domain"],
        "numberofemployees": company["headcount"]["latest_count"],
        "recent_funding_round": company["funding_and_investment"]["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["company_name"],
        "domain": company["company_website_domain"],
        "numberofemployees": company["headcount"]["latest_count"],
        "recent_funding_round": company["funding_and_investment"]["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["company_name"],
        "domain": company["company_website_domain"],
        "numberofemployees": company["headcount"]["latest_count"],
        "recent_funding_round": company["funding_and_investment"]["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.

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.

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:

  1. Install the Crustdata MCP server and point your Claude client at it, then add your API token.

  2. Describe the workflow in plain language: ask Claude to find companies that match your ICP, enrich them, and keep the ones that fit.

  3. 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.

  4. 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. Clay remains a strong fit for hands-on, modular client work. Crustdata is the data layer for teams building programmatic outbound and internal tools they want to own end to end.

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 for Crustdata's free tier with 100 credits to test the enrichment calls, or book a demo to walk through the architecture for your stack.

Data

Delivery Methods

Use Cases

Solutions