Build a Clay Waterfall to Find Verified Work Emails
The problem
A single data provider misses many contacts, so reps either work incomplete lists or pay for several tools and copy results between them by hand.
Implementation steps
- 1
Start from a list with first name, last name, and company domain (a CRM export, an event list, or a Clay search), and normalize domains so "www." and trailing paths are removed
- 2
Add the first email provider, for example Apollo, as step one of the waterfall, using your own API key or Clay credits
- 3
Add a second and third provider, for example Hunter and then Prospeo, that only run when the previous step returned no email, so you never pay twice for the same contact
- 4
Run every found email through a verification provider such as ZeroBounce or NeverBounce, keep results marked valid, and send catch-all or unknown results to a separate review list
- 5
Record which provider found each email in a source column so you can compare hit rates per provider on your own data and reorder the waterfall
- 6
Write verified emails and the source back to HubSpot or Salesforce without overwriting emails a rep entered by hand
Template outline
The structure to recreate in your own workspace. Field names and thresholds are starting points.
Clay Table or Workflow:
1. Input: first_name, last_name, company_domain, crm_id
2. Formula: clean_domain = domain without "www." or paths
3. Waterfall, stop at the first result:
- Provider 1: Apollo find email
- Provider 2: Hunter find email (only if provider 1 is empty)
- Provider 3: Prospeo find email (only if providers 1 and 2 are empty)
4. Verify: ZeroBounce or NeverBounce
- Keep: valid
- Review list: catch-all or unknown
- Drop: invalid
5. Column: email_source = provider that returned the kept email
6. CRM write-back: email, email_source, verified_on (skip if a rep-entered email exists)
Output: one verified email per contact where any provider finds one, with the source recorded for tuning provider orderBuild it in Clay
Copy these commands into your own Clay CLI. They run in your Clay workspace under your login. Nothing runs on our side, and this site never sees your Clay data or login.
This builds the waterfall as a Clay workflow. A CSV trigger sends each contact through two email finders in order, a verification step, and a CRM update. Each CSV row becomes one workflow run. The Clay CLI cannot create tables, add columns, or import rows into a table, so this build uses a workflow; in a Clay table the same waterfall is a set of enrichment columns.
Not tested by GTM Playbooks
This build was written from the Clay CLI v1.3.1 help output, not from a run. Anything marked “Verify in your workspace”, and every line tagged VERIFY in the script, uses a shape that help does not document. Workflow runs can spend your Clay credits.
Part 1: Before you start
1.Install the Clay CLI
Use version 1.3.1 or later, installed from Clay's own instructions. This page does not include an install command because the CLI reference it was written from does not list one. You also need jq, which the script uses to read the JSON the CLI prints.
2.Log in to your workspace
Browser login is the default. Use the device flow on a headless machine. A static key in the CLAY_API_KEY environment variable also works. Workflow commands need the terracotta:cli scope, so an auth error that names it means your login cannot manage workflows.
Log in with a browser
clay loginLog in on a headless machine
clay login --deviceConfirm who you are logged in as
clay whoami3.Pick the workspace to build in
The workflow is created in your active workspace. Switch first if you belong to more than one.
List workspaces
clay workspaces listShow the active workspace
clay workspaces currentSwitch workspace
clay workspaces switch <workspaceId>4.Find your action keys
Action package ids and keys are specific to your workspace. Save the catalog, search it for two email finders, an email verifier, and your CRM update action, then paste their ids into the top of the script. Skip "clay workflows actions test": it spends credits.
Save the action catalog
clay workflows actions list > catalog.jsonSearch the catalog
grep -Eio '"[^"]*(apollo|hunter|prospeo|zerobounce|neverbounce|hubspot|salesforce)[^"]*"' catalog.json | sort -uSee an action's inputs and outputs
clay workflows actions schema <packageId> <actionKey>5.Connect your providers and CRM
Connect the email finders, the verification provider, and HubSpot or Salesforce inside Clay, or use Clay credits for providers that support it. Use a test CRM record for the first run.
Part 2: Workflow spec
What the script builds: Waterfall email enrichment.
Trigger
csv_uploadYou upload a CSV of contacts to the trigger. Each header becomes a string input, and each row becomes one run once the workflow is published.
Verify in your workspace: The trigger type string "csv_upload" is inferred. The CLI help does not show how to create a CSV trigger.
Inputs (CSV headers)
first_name- Contact first name
- Jane
last_name- Contact last name
- Doe
company_domain- Company domain, without www
- example.com
crm_id- HubSpot or Salesforce record ID to update
- your-test-record-id
- 1
Find email with provider 1
actionThe first email finder in the waterfall, usually the provider with the best coverage for your market.
- Reads
first_namelast_namecompany_domain- Writes
email_1
Verify in your workspace: Pick the provider from your catalog. Providers you have not connected in Clay may not appear.
- 2
Find email with provider 2
actionRuns only when step 1 found nothing, so you do not pay twice for the same contact.
- Reads
first_namelast_namecompany_domain- Writes
email_2
Verify in your workspace: The "only when step 1 is empty" rule needs a run condition. The CLI lists a run_condition step kind but does not document its shape, so set the condition in the Clay UI.
- 3
Verify the email
actionChecks deliverability and keeps valid results. Send catch-all and unknown results to a review list.
- Reads
email_1 or email_2- Writes
emailemail_status
- 4
Update the CRM record
actionWrites the verified email and the provider that found it to the CRM record.
- Reads
crm_idemailemail_source- Writes
CRM response
Verify in your workspace: Map the update so it skips records where a rep already entered an email. Check the update action's fields with "clay workflows actions schema".
Part 3: Build script
Creates the workflow, trigger, and four steps, then pauses so you can map step inputs and the run condition. It validates the graph, offers one test run, and asks before publishing.
Save it as build-waterfall-enrichment.sh, fill in the 8 action ids at the top, then run bash build-waterfall-enrichment.sh.
build-waterfall-enrichment.sh
#!/usr/bin/env bash# Builds the "Waterfall email enrichment" Clay workflow# in YOUR Clay workspace, using YOUR Clay CLI login.# Written against Clay CLI v1.3.1. Not run or tested by GTM Playbooks.# Lines tagged VERIFY use a shape the CLI help does not document.set -euo pipefail # Copy these from catalog.json (see "Find your action keys").EMAIL_A_PACKAGE_ID="" # First email finder (for example Apollo)EMAIL_A_ACTION_KEY=""EMAIL_B_PACKAGE_ID="" # Fallback finder (for example Hunter or Prospeo)EMAIL_B_ACTION_KEY=""VERIFIER_PACKAGE_ID="" # Email verification (for example ZeroBounce)VERIFIER_ACTION_KEY=""CRM_PACKAGE_ID="" # HubSpot or Salesforce: update recordCRM_ACTION_KEY="" # One input to test before real traffic. Use yourself or a teammate.TEST_ROW='{"first_name":"","last_name":"","company_domain":"","crm_id":""}' for v in EMAIL_A_PACKAGE_ID EMAIL_A_ACTION_KEY EMAIL_B_PACKAGE_ID EMAIL_B_ACTION_KEY VERIFIER_PACKAGE_ID VERIFIER_ACTION_KEY CRM_PACKAGE_ID CRM_ACTION_KEY; do [[ -n "${!v}" ]] || { echo "Set $v at the top of this script." >&2; exit 1; }donecommand -v clay > /dev/null || { echo "Clay CLI not found on PATH." >&2; exit 1; }command -v jq > /dev/null || { echo "jq is required to read CLI output." >&2; exit 1; }clay whoami > /dev/null || { echo "Not logged in. Run: clay login" >&2; exit 1; } # VERIFY: only "workflows create" documents an "id" field in its output.read_id() { local id id=$(jq -r '.id // empty' "$1") [[ -n "$id" ]] || { echo "No id found in $1. Open it and adjust read_id." >&2; exit 1; } echo "$id"} mkdir -p clay-waterfall-enrichmentcd clay-waterfall-enrichment echo "1/8 Create the workflow"clay workflows create --name "Waterfall email enrichment" > workflow.jsonWF_ID=$(read_id workflow.json)WF_URL=$(jq -r '.url // empty' workflow.json) echo "2/8 Add the CSV upload trigger"# VERIFY: triggerType "csv_upload" is inferred; the help does not show it.clay workflows triggers create "$WF_ID" --input '{"triggerType":"csv_upload"}' > trigger.jsonTRG_ID=$(read_id trigger.json) echo "3/8 Step 1: find email with provider 1"# VERIFY: help shows nodeType and name only; actionPackageId and actionKey are a guess.STEP1=$(jq -nc --arg p "$EMAIL_A_PACKAGE_ID" --arg k "$EMAIL_A_ACTION_KEY" '{nodeType: "tool", name: "Find email with provider 1", actionPackageId: $p, actionKey: $k}')clay workflows nodes create "$WF_ID" --input "$STEP1" > step1.jsonSTEP1_ID=$(read_id step1.json) echo "4/8 Step 2: find email with provider 2"# VERIFY: action fields inside "step" are inferred from the documented utility example.STEP2=$(jq -nc --arg n "$STEP1_ID" --arg p "$EMAIL_B_PACKAGE_ID" --arg k "$EMAIL_B_ACTION_KEY" '{target: {type: "after-node", nodeId: $n}, step: {kind: "action", actionPackageId: $p, actionKey: $k}}')clay workflows nodes insert "$WF_ID" --input "$STEP2" > step2.jsonSTEP2_ID=$(read_id step2.json) echo "5/8 Step 3: verify the email"# VERIFY: same inferred insert shape as step 2.STEP3=$(jq -nc --arg n "$STEP2_ID" --arg p "$VERIFIER_PACKAGE_ID" --arg k "$VERIFIER_ACTION_KEY" '{target: {type: "after-node", nodeId: $n}, step: {kind: "action", actionPackageId: $p, actionKey: $k}}')clay workflows nodes insert "$WF_ID" --input "$STEP3" > step3.jsonSTEP3_ID=$(read_id step3.json) echo "6/8 Step 4: update the crm record"# VERIFY: same inferred insert shape as step 2.STEP4=$(jq -nc --arg n "$STEP3_ID" --arg p "$CRM_PACKAGE_ID" --arg k "$CRM_ACTION_KEY" '{target: {type: "after-node", nodeId: $n}, step: {kind: "action", actionPackageId: $p, actionKey: $k}}')clay workflows nodes insert "$WF_ID" --input "$STEP4" > step4.jsonclay workflows graph get "$WF_ID" --mode full > graph.json echo "7/8 Map step inputs"cat <<EOF Step inputs are specific to your workspace. Map them before validating: Step 1 provider 1: first_name, last_name, company_domain Step 2 provider 2: same inputs, with a run condition so it only runs when step 1 returned no email Step 3 verify: the step 1 email, or the step 2 email when step 1 is empty Step 4 CRM update: crm_id, the verified email, and the provider that found itMap them in the Clay UI${WF_URL:+ at $WF_URL}, or per node with: clay workflows nodes get $WF_ID <nodeId> clay workflows nodes update $WF_ID <nodeId> --input node.jsongraph.json in this folder shows the current nodes and edges. EOFread -r -p "Press Enter when inputs are mapped. " echo "8/8 Validate, test once, publish"clay workflows graph validate "$WF_ID"clay workflows diagram "$WF_ID" if jq -e 'all(.[]; . != "")' <<< "$TEST_ROW" > /dev/null; then read -r -p "Run TEST_ROW once? It can spend Clay credits and update a CRM record. [y/N] " answer if [[ "$answer" == [yY] ]]; then clay workflows runs test "$WF_ID" --inputs "$TEST_ROW" > test-run.json echo "Saved test-run.json. Check each step with: clay workflows runs steps $WF_ID <runId>" fielse echo "TEST_ROW is empty, skipping the test run."fi read -r -p "Publish the workflow now? [y/N] " answerif [[ "$answer" == [yY] ]]; then clay workflows publish "$WF_ID" --name v1fi echo "Workflow $WF_ID, CSV upload trigger $TRG_ID. All responses are saved in $(pwd)."Part 4: Contact CSV
Save this as clay-waterfall-enrichment/contacts.csv and replace the sample row with your list. Keep the headers exactly as shown, because they become the trigger inputs.
contacts.csv
first_name,last_name,company_domain,crm_idJane,Doe,example.com,your-test-record-idPart 5: Run it on your list
After publishing, upload the CSV to the trigger and start with a few rows. Each row is one run.
Load the saved ids (run inside clay-waterfall-enrichment/)
WF_ID=$(jq -r '.id' workflow.json); TRG_ID=$(jq -r '.id' trigger.json)Upload the CSV (this does not start runs)
clay workflows triggers csv upload "$TRG_ID" --file contacts.csvRun the first 5 rows (needs a published workflow, one run per row)
clay workflows triggers csv run "$TRG_ID" --limit 5List runs
clay workflows runs list "$WF_ID"Inspect one run
clay workflows runs get "$WF_ID" <runId>See each step's output for that run
clay workflows runs steps "$WF_ID" <runId>Verify in your workspace
The Clay CLI help does not document these details. Check each one before you rely on the build.
- The CSV trigger type "csv_upload" is inferred. If "triggers create" exits with code 2 (validation), check which trigger types your workspace accepts.
- The help documents "nodes create" with nodeType and name only. If it rejects actionPackageId and actionKey, create the node with a name only and pick the action in the Clay UI.
- The reference says action steps take actionPackageId and actionKey but shows only a utility example for "nodes insert", so the placement of those fields inside "step" is inferred.
- Only "workflows create" documents an id field in its response. The script reads .id from trigger and node responses too and stops if it is missing.
- Package ids and action keys differ by workspace and depend on which integrations you have connected. Copy them from your catalog; this page does not guess them.
- Step input mapping is dynamic per workspace. The CLI path is "nodes get" then "nodes update", where incomingEdges is a full replacement. The argument order for "nodes get" is inferred from "nodes update".
- The help does not say whether step 1 connects to the trigger automatically. "clay workflows graph validate" reports a disconnected graph.
- A waterfall stops at the first provider that returns a result. Here that is two finder steps plus a run condition, because the CLI help does not show a built-in waterfall step for workflows.
- "runs test" without --live is assumed to run the draft. The reference lists --live without describing it.
- Workflow runs can spend Clay credits and write to connected tools. Exit code 3 means re-run clay login, and exit code 4 means you are rate limited (wait for details.retryAfter).
Keep going
Related playbooks
- InboundIntermediate
Enrich Inbound Leads with Apollo and Clay Waterfalls
Marketing passes incomplete lead data to sales, forcing reps to research the basics by hand before every first touch.
ClayApolloHubSpotEstimate: 12 hrs/week - OutboundBeginner
Enrich Event Attendee Lists for Post-Event Follow-Up
Event badge scans give you name and company, but no job title, email, or tech stack for targeting.
ClayApolloHubSpotEstimate: 6 hrs/week - OutboundAdvanced
Sync Apollo Saved Searches to Instantly Campaigns
Sales ops manually exports Apollo lists and imports them to Instantly weekly, delaying campaign launches.
ApolloInstantlyMakeEstimate: 4 hrs/week - RevOpsIntermediate
Keep CRM Data Clean with an AI Hygiene Agent
CRM records decay as people change jobs and reps type titles and industries in different formats, so segmentation, routing, and reporting quietly break.
ClayClaygentHubSpot or Salesforce+2Estimate: 4 hrs/week - OutboundIntermediate
Turn Buyer Intent Signals into Outbound Sequences
Intent data shows which accounts are researching your category, but it sits in a dashboard nobody checks daily, so the signal is stale before a rep acts on it.
ClayG2 Buyer Intent or BomboraApollo+2Estimate: 5 hrs/week - InboundIntermediate
Automate Inbound Lead Routing with Enrichment and Fit Scoring
Inbound leads wait in a shared queue or get assigned round-robin regardless of fit, so the best leads reach a rep late and poor-fit leads take up AE time.
ClayHubSpot or SalesforceApollo+2Estimate: 4 hrs/week - InboundIntermediate
Route Product-Qualified Leads from Sign-Ups to Sales
Self-serve sign-ups pile up in the product, and sales either ignores them or chases every free account, so the few sign-ups from ICP companies who are actually using the product wait for a rep.
ClaySegment, Amplitude, or MixpanelHubSpot or Salesforce+2Estimate: 4 hrs/week - RevOpsIntermediate
Keep Clay Credit Spend Predictable on Enrichment Tables
Clay tables that enrich every row, re-run on every edit, and call providers in no particular order burn through credits faster than planned, and the overspend usually shows up only when the monthly balance runs low.
ClayClaygentHubSpot or SalesforceEstimate: 2 hrs/week - OutboundAdvanced
Find Lookalike Accounts from Your Closed-Won Customers
Outbound lists are usually built from broad filters, while the best predictor of who will buy, the customers you already closed, sits in the CRM unused, so reps spend sequences on accounts that look nothing like your wins.
HubSpot or SalesforceClayClaygent+1Estimate: 4 hrs/week - OutboundAdvanced
Build Prospect Lists from Plain-English Requests in Slack
Reps and marketers wait days for RevOps to build a prospect list, or pull their own from Apollo with inconsistent filters, no CRM dedupe, and no check on how many credits the pull will burn.
SlackZapier or MakeOpenAI API+4Estimate: 4 hrs/week
Need help implementing a playbook?
Our team builds custom Clay workflows and AI automations for revenue teams.
Get it built for you