Keep CRM Data Clean with an AI Hygiene Agent
The problem
CRM records decay as people change jobs and reps type titles and industries in different formats, so segmentation, routing, and reporting quietly break.
Implementation steps
- 1
Export contacts not updated in 90 days from HubSpot or Salesforce with record ID, email, name, title, company, domain, and industry, and upload the export to Clay weekly or monthly
- 2
Verify each email and mark hard bounces, then use person enrichment to check whether contacts with invalid emails still work at the company
- 3
Use an AI step (Claygent or a Use AI action) to map free-text titles to a fixed seniority and department list and to fill a missing industry from the company website, returning a confidence value
- 4
Apply changes automatically only when confidence is high and the CRM field is empty or clearly malformed, and send everything else to a review list instead of overwriting rep-entered data
- 5
Write approved fixes back to the CRM with an "AI hygiene reviewed on" date, and mark contacts who left as former employees rather than deleting them
- 6
Post a Slack summary after each run with the number of fields fixed, contacts flagged as left, and records waiting for review
Template outline
The structure to recreate in your own workspace. Field names and thresholds are starting points.
Clay Workflow (CSV trigger, weekly or monthly export):
1. Input: crm_id, email, first_name, last_name, title, company, company_domain, industry
2. Verify email: valid, invalid, catch-all
3. IF invalid: person enrichment to check current employer
4. AI step (Claygent or Use AI), JSON output:
- seniority: C-level, VP, Director, Manager, Individual contributor
- department: Sales, Marketing, RevOps, Finance, Customer Success, Product, Engineering, Other
- industry: fill only if empty
- confidence: high, medium, low
5. IF confidence = high AND field is empty or malformed: update CRM field
ELSE: add to review list
6. CRM: "AI hygiene reviewed on" = today, "Former employee" = true when they left
7. Slack summary: fields fixed, contacts left, records for review
Output: standardized title, department, and industry fields on stale records, with low-confidence changes reviewed by a personBuild 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 hygiene agent as a Clay workflow. A CSV trigger sends each exported CRM record through email verification, an AI step that standardizes title fields and fills missing industry, and a CRM update. Each CSV row becomes one workflow run. The Clay CLI cannot create tables, so this build uses a workflow.
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 an email verifier, a Claygent or Use AI action, 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 '"[^"]*(zerobounce|neverbounce|claygent|use ai|hubspot|salesforce)[^"]*"' catalog.json | sort -uSee an action's inputs and outputs
clay workflows actions schema <packageId> <actionKey>5.Export the records to clean
Export contacts not updated in 90 days from HubSpot or Salesforce with the columns in the sample CSV. Start with a small export that includes a test record.
Part 2: Workflow spec
What the script builds: CRM hygiene agent.
Trigger
csv_uploadYou upload a CRM export 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)
crm_id- HubSpot or Salesforce record ID
- your-test-record-id
email- Email on the record
- jane@example.com
first_name- First name
- Jane
last_name- Last name
- Doe
title- Title as typed in the CRM
- vp sales & ops
company- Company name
- Example Co
company_domain- Company domain
- example.com
industry- Current industry value, may be empty
- (empty)
- 1
Verify the email
actionFlags hard bounces, which often mean the person left the company.
- Reads
email- Writes
email_status
- 2
Standardize title and fill firmographics
actionA Claygent or Use AI step maps free-text titles to fixed lists and fills a missing industry, with a confidence value.
- Reads
titlecompanycompany_domainindustry- Writes
senioritydepartmentindustryconfidence
Prompt
You clean CRM records. Given this contact and company: title: {{title}} company: {{company}} ({{company_domain}}) current industry: {{industry}} Return JSON with: - seniority: one of C-level, VP, Director, Manager, Individual contributor - department: one of Sales, Marketing, RevOps, Finance, Customer Success, Product, Engineering, Other - industry: the company's industry if current industry is empty, otherwise the current value - confidence: high, medium, or low for the whole record Use the company website for industry. If you are unsure, return low confidence and leave fields empty.
- 3
Update the CRM record
actionWrites high-confidence fixes and an "AI hygiene reviewed on" date back to the record.
- Reads
crm_idsenioritydepartmentindustryemail_status- Writes
CRM response
Verify in your workspace: Writing only high-confidence fields needs a run condition or field-level logic, which the CLI help does not document. Set it in the Clay UI, and do not map fields that would overwrite rep-entered data.
Part 3: Build script
Creates the workflow, trigger, and three steps, then pauses so you can map step inputs and the confidence rule. It validates the graph, offers one test run, and asks before publishing.
Save it as build-crm-hygiene.sh, fill in the 6 action ids at the top, then run bash build-crm-hygiene.sh.
build-crm-hygiene.sh
#!/usr/bin/env bash# Builds the "CRM hygiene agent" 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").VERIFIER_PACKAGE_ID="" # Email verification (for example ZeroBounce)VERIFIER_ACTION_KEY=""AGENT_PACKAGE_ID="" # Claygent or Use AIAGENT_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='{"crm_id":"","email":"","first_name":"","last_name":"","title":"","company":"","company_domain":""}' for v in VERIFIER_PACKAGE_ID VERIFIER_ACTION_KEY AGENT_PACKAGE_ID AGENT_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-crm-hygienecd clay-crm-hygiene echo "1/7 Create the workflow"clay workflows create --name "CRM hygiene agent" > workflow.jsonWF_ID=$(read_id workflow.json)WF_URL=$(jq -r '.url // empty' workflow.json) echo "2/7 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/7 Step 1: verify the email"# VERIFY: help shows nodeType and name only; actionPackageId and actionKey are a guess.STEP1=$(jq -nc --arg p "$VERIFIER_PACKAGE_ID" --arg k "$VERIFIER_ACTION_KEY" '{nodeType: "tool", name: "Verify the email", actionPackageId: $p, actionKey: $k}')clay workflows nodes create "$WF_ID" --input "$STEP1" > step1.jsonSTEP1_ID=$(read_id step1.json) echo "4/7 Step 2: standardize title and fill firmographics"# VERIFY: action fields inside "step" are inferred from the documented utility example.STEP2=$(jq -nc --arg n "$STEP1_ID" --arg p "$AGENT_PACKAGE_ID" --arg k "$AGENT_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/7 Step 3: update the crm record"# VERIFY: same inferred insert shape as step 2.STEP3=$(jq -nc --arg n "$STEP2_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 "$STEP3" > step3.jsonclay workflows graph get "$WF_ID" --mode full > graph.json echo "6/7 Map step inputs"cat <<EOF Step inputs are specific to your workspace. Map them before validating: Step 1 verify: email Step 2 agent: title, company, company_domain, industry, and the agent prompt Step 3 CRM: crm_id plus only the fields step 2 returned with high confidence; send low-confidence rows to a review list insteadMap 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 "7/7 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: CRM export CSV
Save this as clay-crm-hygiene/crm-export.csv and replace the sample row with your export. Keep the headers exactly as shown, because they become the trigger inputs.
crm-export.csv
crm_id,email,first_name,last_name,title,company,company_domain,industryyour-test-record-id,jane@example.com,Jane,Doe,vp sales & ops,Example Co,example.com,Part 5: Run it on your export
After publishing, upload the export to the trigger and start with a few rows. Each row is one run.
Load the saved ids (run inside clay-crm-hygiene/)
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 crm-export.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.
- This build does not merge duplicates. Handle those separately before or after the hygiene run.
- "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
- RevOpsAdvanced
Prevent Duplicate Contacts Across HubSpot and Salesforce
Marketing creates contacts in HubSpot while sales creates them in Salesforce, leading to duplicates and broken reporting.
HubSpotSalesforceZapierEstimate: 5 hrs/week - RevOpsIntermediate
Build a Clay Waterfall to Find Verified Work Emails
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.
ClayApolloHunter or Prospeo+2Estimate: 4 hrs/week - OutboundIntermediate
Track Champion Job Changes to Open New Accounts
Former champions who move to a new company are some of the warmest prospects you have, but most teams notice the move months later, after the new team has picked its tools.
ClayHubSpot or SalesforceApollo+2Estimate: 3 hrs/week - RevOpsBeginner
Sync Salesforce Pipeline to Google Sheets Dashboard
Executives ask for pipeline reports in spreadsheets, but pulling data manually creates version control chaos.
ZapierSalesforceGoogle SheetsEstimate: 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 - RevOpsAdvanced
Run CRM-Writing AI Agents in Dry-Run Mode with Bulk Approvals
An AI agent that writes straight to Salesforce or HubSpot can change thousands of records before anyone notices a bad rule, and each change can trigger syncs, workflows, and sequences that are hard to trace and harder to reverse.
HubSpot or SalesforceClayGoogle Sheets+1Estimate: 2 hrs/week
Need help implementing a playbook?
Our team builds custom Clay workflows and AI automations for revenue teams.
Get it built for you