---
name: pql-signups-to-sales
description: "Turn product sign-ups into PQLs: enrich them in Clay, score ICP fit plus in-product activity, and route qualified ones to reps in HubSpot or Salesforce. Use when 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."
---

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

## When to use this skill

- The user wants to set up or run this workflow: "Route Product-Qualified Leads from Sign-Ups to Sales".
- The user describes this problem: 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.

## At a glance

- Category: Inbound
- Difficulty: Intermediate
- Time saved: 4 hours per week. This figure is an estimate, not a measured result.

## Tools

Options separated by "or" are alternatives; use whichever the user already has.

- Clay
- Segment, Amplitude, or Mixpanel
- HubSpot or Salesforce
- OpenAI API
- Slack

## Steps

1. Pick the activation milestone that tends to come before a paid conversion for your product, for example connecting an integration or inviting two teammates in the first 14 days, using your own conversion data in Amplitude, Mixpanel, or your warehouse
2. Send a webhook to Clay when a sign-up hits the milestone, from your product backend, a Segment webhook destination, or a cohort sync from your product analytics tool, with email, workspace ID, plan, sign-up date, and the usage counts behind the milestone
3. Drop personal email domains, then enrich the person and company in Clay (title, seniority, headcount, industry, country) from the work email
4. Score ICP fit as tier A, B, or C with a formula or an AI step, and mark A and B sign-ups that hit the milestone as product-qualified leads; C stays in the self-serve path
5. Create or update the contact and company in HubSpot or Salesforce with a PQL flag, fit tier, reason, milestone, and workspace ID, and let your assignment rules set the owner while keeping any existing account owner
6. Alert the owner in Slack with who signed up, what they did in the product, and why they fit, and have the rep offer help with the use case they started instead of a generic demo pitch

## Template outline

Field names, thresholds, and message text are starting points to adapt.

~~~text
Clay Workflow (webhook trigger, one run per sign-up that hits the milestone):
1. Milestone (from your own data): e.g. connected_integration OR teammates_invited >= 2 within 14 days
2. Webhook payload: email, first_name, last_name, workspace_id, plan, signup_date, milestone, active_days_7d, teammates_invited
3. Skip personal email domains (gmail.com, outlook.com, and similar)
4. Enrich: title, seniority, company, headcount, industry, country
5. AI or formula scoring:
   - fit_tier: A, B, or C against your written ICP
   - is_pql: fit_tier is A or B AND milestone reached
   - reason: one sentence a rep can read
6. CRM upsert: PQL = {{is_pql}}, fit tier, reason, milestone, workspace_id; keep existing owner
7. CRM assignment rules: PQL = true, route by territory and segment
8. Slack to owner (PQL only): "{{first_name}} at {{company}} ({{title}}) hit {{milestone}}: {{reason}}"
Output: product-qualified sign-ups reach an owner in the CRM with the usage and fit behind them, and poor-fit sign-ups stay self-serve
~~~

## Optional: build it with the Clay CLI

This builds PQL routing as a Clay workflow with a webhook trigger. Each sign-up that hits your activation milestone becomes one run: enrich the person and company, score ICP fit against the usage you sent, write the PQL flag and reason to the CRM so its assignment rules pick the owner, and alert the owner in Slack. The Clay CLI cannot create tables, so this build uses a workflow.

Written against Clay CLI v1.3.1. Run these commands only in the user's own Clay workspace, logged in with their own account, after they confirm. Items marked VERIFY are config shapes the CLI help does not document; check them in the workspace before relying on them.

### Prerequisites

- 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.
- 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 login`
  - Log in on a headless machine: `clay login --device`
  - Confirm who you are logged in as: `clay whoami`
- 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 list`
  - Show the active workspace: `clay workspaces current`
  - Switch workspace: `clay workspaces switch <workspaceId>`
- 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.json`
  - Search the catalog: `grep -Eio '"[^"]*(enrich|apollo|use ai|generate|hubspot|salesforce|slack)[^"]*"' catalog.json | sort -u`
  - See an action's inputs and outputs: `clay workflows actions schema <packageId> <actionKey>`
- Pick the milestone and the sender: Choose the activation milestone from your own conversion data, then decide what will POST to the webhook when a sign-up hits it: your product backend, a Segment webhook destination, or a cohort sync from Amplitude or Mixpanel. Send only sign-ups who reached the milestone, and filter out personal email domains before sending if you can.
- Add the CRM fields and connect Slack: Create contact properties for PQL, fit tier, PQL reason, milestone, and workspace ID in HubSpot or Salesforce, connect the CRM and Slack inside Clay, and set the CRM assignment rules that read the PQL flag.

### Workflow: PQL sign-ups to sales

Trigger (webhook): Your backend, a Segment webhook destination, or a product analytics cohort sync POSTs one JSON object per sign-up that hits the milestone to the trigger's webhook URL. The fields arrive as run inputs as-is. VERIFY: The exact payload a Segment destination or an Amplitude or Mixpanel cohort sync sends depends on how you configure it. Map its fields to the inputs below.

Inputs (webhook payload fields):

- email: Sign-up email (example: jane@example.com)
- first_name: First name, if collected (example: Jane)
- last_name: Last name, if collected (example: Doe)
- workspace_id: Account or workspace ID in your product (example: ws_12345)
- plan: Current plan (example: free)
- signup_date: When they signed up (example: 2026-10-09)
- milestone: Which activation milestone they hit (example: connected_integration)
- active_days_7d: Active days in the last 7 (example: 4)
- teammates_invited: Teammates invited so far (example: 2)

1. Enrich the person and company (action): Adds title, seniority, headcount, industry, and country from the work email.
   - Inputs: email
   - Outputs: title, seniority, company, headcount, industry, country
2. Score fit and PQL status (action): An AI text action combines ICP fit with the usage you sent and returns a tier, a PQL flag, and a reason a rep can read.
   - Inputs: title, seniority, headcount, industry, country, milestone, plan, active_days_7d, teammates_invited
   - Outputs: fit_tier, is_pql, reason
   - Prompt:

~~~text
Decide whether this product sign-up is a product-qualified lead.
ICP: {{your_icp_description}}
Person: {{title}} ({{seniority}}) at a {{headcount}}-person {{industry}} company in {{country}}.
Product usage: hit the "{{milestone}}" milestone on plan {{plan}}, {{active_days_7d}} active days in the last 7, {{teammates_invited}} teammates invited.
Return JSON with fit_tier (A, B, or C), is_pql (true when fit_tier is A or B), and reason (one sentence a rep can read).
Do not guess missing fields. Treat a blank title or company as C.
~~~

   - VERIFY: A formula works as well as an AI step if your fit rules are simple. Keep the same three outputs either way.
3. Write the PQL to the CRM (action): Creates or updates the contact and company with PQL, fit tier, reason, milestone, and workspace ID, leaving any existing owner unchanged.
   - Inputs: email, first_name, last_name, company, workspace_id, fit_tier, is_pql, reason, milestone
   - Outputs: owner, CRM record
   - VERIFY: Owner assignment usually works best in the CRM itself (Salesforce assignment rules, or a HubSpot workflow that sets owners by property). This step writes the fields those rules read.
4. Alert the owner in Slack (action): Tells the owner who signed up, what they did in the product, and why they fit. Runs only for PQLs.
   - Inputs: owner, first_name, last_name, title, company, milestone, reason
   - Outputs: Slack message
   - VERIFY: The PQL-only rule needs a run condition, which the CLI help does not document. Set it in the Clay UI.

### Script: build-pql-routing.sh

Creates the workflow, the webhook trigger, and four steps, then pauses so you can map step inputs and the PQL-only condition. It validates the graph, offers one test run, and asks before publishing.

~~~bash
#!/usr/bin/env bash
# Builds the "PQL sign-ups to sales" 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 enrichment from email
ENRICH_ACTION_KEY=""
SCORER_PACKAGE_ID="" # Use AI or another AI text action
SCORER_ACTION_KEY=""
CRM_PACKAGE_ID=""    # HubSpot or Salesforce: create or update
CRM_ACTION_KEY=""
SLACK_PACKAGE_ID=""  # Slack: send message
SLACK_ACTION_KEY=""

# One input to test before real traffic. Use yourself or a teammate.
TEST_ROW='{"email":"","first_name":"","last_name":"","workspace_id":"","plan":"","signup_date":"","milestone":"","active_days_7d":"","teammates_invited":""}'

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; }
done
command -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-pql-routing
cd clay-pql-routing

echo "1/8 Create the workflow"
clay workflows create --name "PQL sign-ups to sales" > workflow.json
WF_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.json
TRG_ID=$(read_id trigger.json)

echo "3/8 Step 1: enrich the person 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 person and company", actionPackageId: $p, actionKey: $k}')
clay workflows nodes create "$WF_ID" --input "$STEP1" > step1.json
STEP1_ID=$(read_id step1.json)

echo "4/8 Step 2: score fit and pql status"
# 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.json
STEP2_ID=$(read_id step2.json)

echo "5/8 Step 3: write the pql to 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.json
STEP3_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.json
clay 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 from the payload
  Step 2 score:  title, seniority, headcount, industry, country, milestone,
                 plan, active_days_7d, teammates_invited, and the prompt
                 with your ICP written in
  Step 3 CRM:    email, name, company, workspace_id, fit_tier, is_pql,
                 reason, milestone; do not change an existing owner
  Step 4 Slack:  owner, name, title, company, milestone, reason; add a
                 run condition so it only runs when is_pql is true
Map 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.json
graph.json in this folder shows the current nodes and edges.

EOF
read -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>"
  fi
else
  echo "TEST_ROW is empty, skipping the test run."
fi

read -r -p "Publish the workflow now? [y/N] " answer
if [[ "$answer" == [yY] ]]; then
  clay workflows publish "$WF_ID" --name v1
fi

echo "Workflow $WF_ID, webhook trigger $TRG_ID. All responses are saved in $(pwd)."
~~~

### Sample milestone payload: sample-signup.json

~~~json
{
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Doe",
  "workspace_id": "ws_12345",
  "plan": "free",
  "signup_date": "2026-10-09",
  "milestone": "connected_integration",
  "active_days_7d": "4",
  "teammates_invited": "2"
}
~~~

### Send it a sign-up

After publishing, read the webhook URL and send the sample payload once. Each POST is one run.

- Load the saved ids (run inside clay-pql-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-signup.json`
- List 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 the workspace

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

## Guardrails

- Work only in the user's own accounts and workspaces, with their credentials.
- Confirm with the user before any step that writes to a CRM, sends email or messages, spends credits, or moves money.
- Treat every hours, cost, and threshold figure as a starting assumption and replace it with the user's numbers.
- Do not invent data, prices, or results. If a vendor price matters, check the vendor's current pricing page.

## Source

https://gtmplaybooks.io/playbooks/pql-signups-to-sales/
