Automate Inbound Lead Routing with Enrichment and Fit Scoring
The problem
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.
Implementation steps
- 1
Send each form submission to a Clay webhook from your form tool or a HubSpot or Salesforce workflow, including email, name, company, and the form or page it came from
- 2
Enrich the lead and company (title, seniority, headcount, industry, country) with Clay and Apollo, using the email domain when the company field is blank
- 3
Score fit with a formula or an AI step against your written ICP, returning a tier (A, B, or C) and a one-line reason
- 4
Pick the owner with rules you can read: the existing account owner first, then territory by country, then segment by headcount, then round-robin within the matching team
- 5
Create or update the lead in HubSpot or Salesforce with the owner, tier, and reason, and never change an existing owner
- 6
Send the owner a Slack message with the lead details and reason for tier A leads, and route tier C leads to a nurture list instead of a rep
Template outline
The structure to recreate in your own workspace. Field names and thresholds are starting points.
Clay Workflow (webhook trigger, one run per form submission):
1. Webhook payload: email, first_name, last_name, company, form, page_url
2. Enrich: title, seniority, headcount, industry, country (fallback: email domain)
3. AI scoring prompt: "Score this lead against our ICP: {{icp}}. Return tier (A, B, C) and a one-sentence reason. Do not guess missing fields."
4. Routing rules, in order:
- Account already has an owner: keep that owner
- Territory: country to team
- Segment: headcount band to team
- Round-robin within the team
5. CRM upsert: owner, tier, reason, lead source = {{form}}
6. Slack: DM the owner for tier A with {{reason}} and {{page_url}}
7. Tier C: add to nurture list, no rep assignment
Output: every inbound lead has an owner, a tier, and a reason in the CRM, and tier A owners hear about it right awayBuild 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 routing as a Clay workflow with a webhook trigger. Each form submission becomes one run: enrich the lead and company, score ICP fit, write the tier and reason to the CRM so its assignment rules can pick the owner, and alert the owner in Slack. 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 person and company enrichment, an AI text action, your CRM create or update action, and Slack, 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 '"[^"]*(enrich|apollo|use ai|generate|hubspot|salesforce|slack)[^"]*"' catalog.json | sort -uSee an action's inputs and outputs
clay workflows actions schema <packageId> <actionKey>5.Connect your CRM and Slack, and plan the form hookup
Connect HubSpot or Salesforce and Slack inside Clay. Decide what will POST form submissions to the webhook: your form tool, a CRM workflow, or a Zapier relay.
Part 2: Workflow spec
What the script builds: Inbound lead routing.
Trigger
webhookYour form tool, a CRM workflow, or a relay POSTs one JSON object per submission to the trigger's webhook URL. The fields arrive as run inputs as-is.
Inputs (webhook payload fields)
email- Work email from the form
- jane@example.com
first_name- First name
- Jane
last_name- Last name
- Doe
company- Company name, may be blank
- Example Co
form- Which form was submitted
- demo-request
page_url- Page the form was on
- https://yourdomain.com/pricing
- 1
Enrich the lead and company
actionAdds title, seniority, headcount, industry, and country, using the email domain when the company field is blank.
- Reads
emailcompany- Writes
titleseniorityheadcountindustrycountry
- 2
Score ICP fit
actionAn AI text action compares the lead to your written ICP and returns a tier with a reason a rep can read.
- Reads
titleseniorityheadcountindustrycountryform- Writes
tierreason
Prompt
Score this inbound lead against our ICP. ICP: {{your_icp_description}} Lead: {{title}} ({{seniority}}) at a {{headcount}}-person {{industry}} company in {{country}}. Form: {{form}} Return JSON with tier (A, B, or C) and reason (one sentence). A means strong fit and a buying role. C means outside the ICP. Do not guess missing fields.
- 3
Assign the owner in the CRM
actionCreates or updates the lead with tier, reason, and source, leaving any existing owner unchanged.
- Reads
emailfirst_namelast_namecompanytierreason- Writes
ownerCRM record
Verify in your workspace: Owner assignment usually works best in the CRM itself (Salesforce assignment rules, or a HubSpot workflow that sets or rotates owners by property). This step writes the fields those rules read.
- 4
Alert the owner in Slack
actionSends the owner the lead, tier, reason, and form for tier A leads.
- Reads
ownertierreasonformpage_url- Writes
Slack message
Verify in your workspace: The tier A rule needs a run condition, which the CLI help does not document. Set it in the Clay UI.
Part 3: Build script
Creates the workflow, the webhook trigger, and four steps, then pauses so you can map step inputs and the tier A condition. It validates the graph, offers one test run, and asks before publishing.
Save it as build-inbound-routing.sh, fill in the 8 action ids at the top, then run bash build-inbound-routing.sh.
build-inbound-routing.sh
#!/usr/bin/env bash# Builds the "Inbound lead routing" 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").ENRICH_PACKAGE_ID="" # Person and company enrichmentENRICH_ACTION_KEY=""SCORER_PACKAGE_ID="" # Use AI or another AI text actionSCORER_ACTION_KEY=""CRM_PACKAGE_ID="" # HubSpot or Salesforce: create or updateCRM_ACTION_KEY=""SLACK_PACKAGE_ID="" # Slack: send messageSLACK_ACTION_KEY="" # One input to test before real traffic. Use yourself or a teammate.TEST_ROW='{"email":"","first_name":"","last_name":"","company":"","form":"","page_url":""}' for v in ENRICH_PACKAGE_ID ENRICH_ACTION_KEY SCORER_PACKAGE_ID SCORER_ACTION_KEY CRM_PACKAGE_ID CRM_ACTION_KEY SLACK_PACKAGE_ID SLACK_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-inbound-routingcd clay-inbound-routing echo "1/8 Create the workflow"clay workflows create --name "Inbound lead routing" > workflow.jsonWF_ID=$(read_id workflow.json)WF_URL=$(jq -r '.url // empty' workflow.json) echo "2/8 Add the webhook trigger"clay workflows triggers create "$WF_ID" --input '{"triggerType":"webhook","inputSchema":{"type":"object"}}' > trigger.jsonTRG_ID=$(read_id trigger.json) echo "3/8 Step 1: enrich the lead and company"# VERIFY: help shows nodeType and name only; actionPackageId and actionKey are a guess.STEP1=$(jq -nc --arg p "$ENRICH_PACKAGE_ID" --arg k "$ENRICH_ACTION_KEY" '{nodeType: "tool", name: "Enrich the lead and company", actionPackageId: $p, actionKey: $k}')clay workflows nodes create "$WF_ID" --input "$STEP1" > step1.jsonSTEP1_ID=$(read_id step1.json) echo "4/8 Step 2: score icp fit"# VERIFY: action fields inside "step" are inferred from the documented utility example.STEP2=$(jq -nc --arg n "$STEP1_ID" --arg p "$SCORER_PACKAGE_ID" --arg k "$SCORER_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: assign the owner in the crm"# 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.jsonSTEP3_ID=$(read_id step3.json) echo "6/8 Step 4: alert the owner in slack"# VERIFY: same inferred insert shape as step 2.STEP4=$(jq -nc --arg n "$STEP3_ID" --arg p "$SLACK_PACKAGE_ID" --arg k "$SLACK_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 enrich: email and company from the form payload (use the email domain when company is blank) Step 2 score: title, seniority, headcount, industry, country, form, and the scoring prompt with your ICP written in Step 3 CRM: email, name, company, tier, and reason; do not change an existing owner Step 4 Slack: owner, tier, reason, and form; add a run condition for tier AMap 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, write to your CRM, and post to Slack. [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, webhook trigger $TRG_ID. All responses are saved in $(pwd)."Part 4: Sample form payload
Save this as clay-inbound-routing/sample-lead.json. Your form hookup should send the same fields for every submission.
sample-lead.json
{ "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "company": "Example Co", "form": "demo-request", "page_url": "https://yourdomain.com/pricing"}Part 5: Send it leads
After publishing, read the webhook URL and send the sample payload once. Each POST is one run.
Load the saved ids (run inside clay-inbound-routing/)
WF_ID=$(jq -r '.id' workflow.json); TRG_ID=$(jq -r '.id' trigger.json)Read the webhook URL (argument order inferred, see the verify notes)
WEBHOOK_URL=$(clay workflows triggers get "$WF_ID" "$TRG_ID" | jq -r '.webhookUrl')Send the sample payload (needs a published workflow, one run per request)
curl -sS -X POST "$WEBHOOK_URL" -H 'Content-Type: application/json' --data @sample-lead.jsonList 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 webhook trigger shape comes from the CLI help, but the help does not say how "triggers get" orders its arguments. If the run commands fail, copy the webhook URL from the Clay UI instead.
- 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.
- "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
Qualify Inbound Leads with AI Before Routing
SDRs lose a meaningful share of their day to unqualified inbound leads (students, competitors, wrong market).
ClayOpenAI APIHubSpotEstimate: 9 hrs/week - 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 - InboundIntermediate
Turn LinkedIn Ad Engagers into Sequenced Outbound
People who like, comment, or click LinkedIn ads are warm, but the engagers never make it into CRM or a sequence unless someone exports by hand.
LinkedIn AdsClayApollo+2Estimate: 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 - 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 - InboundIntermediate
Rebook Demo No-Shows with a Short Personalized Sequence
Booked demos that end as no-shows usually get one "sorry we missed you" email or nothing at all, so meetings the team already worked to create quietly drop out of the pipeline.
Calendly or Chili PiperHubSpot or SalesforceClay+2Estimate: 2 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 - ResearchIntermediate
Send Reps a Researched Account Brief Before Every Meeting
Reps walk into first calls after a quick scan of LinkedIn and the CRM, or skip prep entirely when the calendar is full, so they miss recent company news, past conversations, and open deal context the team already has.
Calendly or Chili PiperZapier or MakeClay+3Estimate: 3 hrs/week - InboundAdvanced
Answer Demo Requests Within Minutes, Even After Hours
Demo requests that come in at night or over the weekend wait until a rep logs on, so the buyer who was ready to talk on Friday evening gets a first reply on Monday, often after they have booked with someone else.
HubSpot or SalesforceClayOpenAI API+3Estimate: 3 hrs/week
Need help implementing a playbook?
Our team builds custom Clay workflows and AI automations for revenue teams.
Get it built for you