A lead fills in your contact form at 4:50 on a Friday. It lands in a shared inbox. The first person to open it is you, on Monday, after coffee. By then the prospect has heard back from two of your competitors, and the one who replied first is the one they're talking to.
This is the n8n workflow we build to stop that happening, written up the way you'd want it if you were paying for it. What it does. What each step costs you. Where the model is allowed to decide and where it isn't. And the actual template you can paste into your own n8n. It's the same shape as the pipeline we migrated for a client from Make, where the bill went from about $109 a month to about $12. It uses only what you already have: a form, a CRM, Slack, and one API key.
Quick answer
The model scores. Your code decides. Your team is first.
A form submission hits a webhook, the workflow fetches the lead's website, sends the form and the page text to a model with a fixed answer format, gets back a score from 0 to 100 with reasons, and then decides the tier in code from your thresholds. Hot leads reach your team in Slack with a five-second brief, every lead lands in your CRM, and the prospect gets a fixed acknowledgement within a minute, at any hour. The scoring call costs about a cent and a half. The person it replaces costs you about $2.93 a lead and gets there three hours later.
<60sForm to Slack brief, any hour
$0.014Scoring call per lead, Opus 5
7xQualification odds inside the first hour (HBR)
What the workflow does, in one screen
Thirteen working nodes, six stages. Every stage writes what it found onto the lead, so nothing downstream has to guess, and you can see at any node exactly what the next one will receive.
Catch the form
Where does the lead arrive?A webhook takes the POST from your form, replies instantly so the form never hangs, and a Settings node holds the six things you'll edit: company name, ideal-customer paragraph, two thresholds, Slack channel, reply promise.
Normalise and check
Is there enough to work with?Name, email, company, website and message are cleaned into fixed fields. No email address or no message means the lead goes straight to a person, with the raw form attached.
Fetch the website
What does this company actually do?A plain HTTP request to the lead's site, eight-second timeout, allowed to fail. A Code node strips it to a title, a description and about 3,000 characters of text, and flags free-mail addresses.
Score with a model
How good is this lead, and why?One HTTP call to the Messages API with a JSON schema the answer must match: score, fit reasons, concerns, next step, two-sentence summary. Low effort setting, one-second-ish latency, a cent and a half.
Decide the tier in code
Hot, warm, cold, or review?A Code node compares the score with your thresholds. The model never picks the route. Anything that isn't a clean integer score becomes "review" and goes to a person.
Route
Who needs to know, and what do they see?Hot leads: Slack brief, CRM record, acknowledgement email. Warm: CRM and acknowledgement. Cold: CRM only. Review: a Slack message that says why.
You'll be sold lead scoring as an intelligence feature. It's a clock feature. The intelligence is nice. The minutes are your money.
In 2011, Harvard Business Review published an audit of 2,241 US companies, each sent a web-generated test lead. Only 37% responded within an hour. Another 16% took between one and 24 hours, 24% took more than a day, and 23% never responded at all. Among the companies that did respond within 30 days, the average first response took 42 hours. The same authors then looked at 1.25 million leads across 42 companies. Firms that tried to contact a prospect within an hour of the enquiry were nearly seven times as likely to qualify the lead as those that tried even an hour later. They were more than 60 times as likely as companies that waited a day or more.
Fifteen years on, the queue in most small businesses looks the same: a submissions folder someone checks two or three times a day. Every lead in that folder crosses HBR's one-hour line. If your pipeline acknowledges, scores and routes in under a minute, none of them do, including the ones that arrive at 4:50 on a Friday.
What one lead costs to score
Here's the arithmetic on a single inbound lead, using the same $22 loaded hour we use across the agent cost article. Handling one lead by hand means reading the form, looking the company up, deciding who should have it, creating the record and telling the rep. We take that as eight minutes. That's our working assumption, and you should substitute yours.
∑One inbound lead, costed
$22 loaded hour
Who or what handles the lead
Per lead
500 leads a month
A person, eight minutes at $22 an hour
$2.93
$1,467 and 67 hours
Scoring call, Claude Opus 5 (about 1,500 tokens in, 260 out)
$0.014
$7.07
Scoring call, Claude Haiku 4.5, same tokens
$0.003
$1.42
Website fetch and text extraction
$0
$0
Notice what the table doesn't say. It doesn't say the pipeline saves you $1,467 a month, because the eight minutes don't disappear. Your salesperson still reads the brief and still makes the call. What changes for you is that the reading happens within a minute of the form arriving, and it starts with a summary instead of a blank record.
What the model decides, and what it doesn't
This is the part the template libraries skip, so we'll be precise about it for you, because it's what makes the workflow safe to leave running.
The model gets the form, the page text and your ideal-customer paragraph. It returns exactly one thing: a JSON object that matches a schema you set. A score from 0 to 100, up to three fit reasons citing evidence, up to three concerns, a recommended first step, and a two-sentence summary. The request uses the API's structured-output format, so the answer is valid JSON that fits the schema or it isn't returned at all. That matters in a pipeline. A free-text answer with a stray sentence in it breaks your next node at 2am.
Then the model's job ends. A few lines of code decide the tier by comparing the score with the two thresholds in your Settings node. A Switch node decides the route by reading the tier. The CRM write is a fixed field mapping. The acknowledgement email is a template with three substitutions: first name, your company name, and your reply promise. Nothing the model wrote reaches your prospect.
▤Which actions run on their own
Review costed at $0.73
Action in the workflow
If it's wrong, it costs you
Expected loss at 95% accuracy
Runs unsupervised?
Create or update the CRM contact
About $0.50, someone corrects a field
$0.03
Yes
Post the brief to Slack
About $0.20, a rep ignores it
$0.01
Yes
Send the fixed acknowledgement
About $1, a templated line
$0.05
Yes
Send an AI-written personalised reply
About $40, a wrong claim loses the lead
$2.00
No, gate it
Book a meeting on a rep's calendar
About $15, a rep's half hour
$0.75
No, gate it
The same logic covers the model declining to answer. If the API returns a refusal, or anything that isn't a clean integer score, the Code node doesn't guess. It marks the lead "review" and sends you a Slack message saying why. Silent failure is the one thing your lead pipeline must never do, because a lead that vanishes looks exactly like a quiet week.
The template, node by node
Thirteen working nodes plus four sticky notes with the setup instructions. You edit one node; the rest run as they are.
⋮The thirteen working nodes
You edit one
Node
What it does
You edit it?
Lead form
Webhook, POST, replies immediately. Carries a pinned sample submission so you can test without a form.
Only the path
Settings
Company name, ideal-customer paragraph, hot and warm thresholds, Slack channel, reply promise.
Email contains @ and the message isn't empty. Otherwise straight to a person.
No
Fetch the company website
GET the site, 8-second timeout, follows redirects, allowed to fail.
No
Prepare the brief
Strips the HTML to title, description and 3,000 characters. Flags free-mail domains.
No
Score with Claude
One POST to the Messages API with the schema. Header Auth credential holds your key.
Credential only
Read the score
Parses the JSON, sets the tier from your thresholds, routes refusals to review.
No
Route by tier
Switch on hot / warm / cold, with review as the fallback output.
No
Alert the team
Slack brief for hot leads: score, summary, reasons, concerns, first step, the original message.
Channel comes from Settings
Create or update the contact
HubSpot upsert on email: first name, last name, company, website, lifecycle stage.
Swap for your CRM
Acknowledge the lead
Gmail, fixed text, three substitutions, sent to hot and warm leads.
The wording, if you like
Needs a human look
Slack message for incomplete forms and review-tier leads, with the reason.
No
The scoring call is deliberately a plain HTTP Request node rather than a vendor node. Two reasons. It survives n8n upgrades, because the HTTP node's shape hasn't changed in years while the AI nodes get rewritten every few months. And it's provider-neutral. To use a different model you change a URL, two headers and a body, and you keep the JSON schema so the tier code still works.
Get the template
Copy the JSON below, open a new workflow in n8n, and paste it onto the canvas. n8n creates all seventeen nodes with the connections and the sticky notes. Then follow the yellow overview note. Edit Settings, add a Header Auth credential named x-api-key for the scoring node, select your Slack, HubSpot and Gmail credentials, and click Test workflow. The pinned sample lead runs the whole thing end to end before you've connected a real form.
{ }workflow.json
17 KB · paste into n8n
workflow.json, 13 working nodes and 4 notes, checked node by node against the n8n package it runs in.
{
"name": "Score inbound leads with Claude and route them to HubSpot, Slack and Gmail",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "new-lead",
"responseMode": "onReceived",
"options": {}
},
"id": "amp-lead-01",
"name": "Lead form",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
300
],
"webhookId": "2c9a6f1e-lead-routing-amplence"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cond-01",
"name": "companyName",
"type": "string",
"value": "Your Company"
},
{
"id": "cond-02",
"name": "icp",
"type": "string",
"value": "Ideal customer: a business with 10 to 250 staff that runs a repetitive, document- or email-heavy process (quotes, intake, support, bookings) and wants it automated with n8n or a custom AI agent. Good signs: a named process, a volume figure, a timeline, a business email domain. Weak signs: students, job seekers, agencies offering their own services, free-mail addresses with no company, requests for a free tool."
},
{
"id": "cond-03",
"name": "hotThreshold",
"type": "number",
"value": 70
},
{
"id": "cond-04",
"name": "warmThreshold",
"type": "number",
"value": 40
},
{
"id": "cond-05",
"name": "slackChannel",
"type": "string",
"value": "#leads"
},
{
"id": "cond-06",
"name": "replyWithin",
"type": "string",
"value": "one working day"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "amp-lead-02",
"name": "Settings",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
220,
300
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "cond-07",
"name": "name",
"type": "string",
"value": "={{ String($json.body.name || $json.body.full_name || '').trim() }}"
},
{
"id": "cond-08",
"name": "email",
"type": "string",
"value": "={{ String($json.body.email || '').trim().toLowerCase() }}"
},
{
"id": "cond-09",
"name": "company",
"type": "string",
"value": "={{ String($json.body.company || $json.body.organisation || '').trim() }}"
},
{
"id": "cond-10",
"name": "websiteUrl",
"type": "string",
"value": "={{ (function (w) { w = String(w || '').trim(); if (!w) return ''; return /^https?:\\/\\//i.test(w) ? w : 'https://' + w; })($json.body.website) }}"
},
{
"id": "cond-11",
"name": "message",
"type": "string",
"value": "={{ String($json.body.message || $json.body.details || '').trim() }}"
},
{
"id": "cond-12",
"name": "source",
"type": "string",
"value": "={{ $json.body.source || 'website form' }}"
},
{
"id": "cond-13",
"name": "receivedAt",
"type": "string",
"value": "={{ $now.toISO() }}"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "amp-lead-03",
"name": "Normalise the lead",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
440,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "cond-14",
"leftValue": "={{ $json.email }}",
"rightValue": "@",
"operator": {
"type": "string",
"operation": "contains"
}
},
{
"id": "cond-15",
"leftValue": "={{ $json.message }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "amp-lead-04",
"name": "Has the basics?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
660,
300
]
},
{
"parameters": {
"url": "={{ $json.websiteUrl }}",
"options": {
"redirect": {
"redirect": {
"followRedirects": true
}
},
"response": {
"response": {
"responseFormat": "text"
}
},
"timeout": 8000
}
},
"id": "amp-lead-05",
"name": "Fetch the company website",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
880,
200
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// Turns the fetched website into a short text sample and joins it to the lead.\n// Nothing here calls a model; it only prepares what the model will read.\nconst lead = $('Normalise the lead').first().json;\nconst page = $input.first().json;\nconst html = typeof page.data === 'string' ? page.data : '';\n\nconst strip = (h) => h\n .replace(/
We validate the file the way you'd want any template validated. Every node type and version is checked against the n8n package it will run in. Every parameter name is checked against what that node actually declares. Every connection is checked against a node that exists. That doesn't prove the workflow will do what you want with your data. It proves it will load and run without you debugging our typos first. The same treatment for an online store's support inbox is in our Shopify customer support workflow.
Where the platform bill lands
You don't need a big volume for the economics to matter, and you don't need to self-host on day one. But the shape of the bill is worth knowing before you pick a platform, because this pipeline is exactly the kind that per-step billing punishes you for.
Each lead through this template is one n8n execution. On a per-operation platform it's about thirteen credits, one per node. At 500 leads a month that's 6,500 credits against 500 executions. At 2,000 leads it's 26,000 against 2,000. The client pipeline we migrated had this shape, with a lead-scoring model call in the middle. At around 90,000 operations a month its Make bill had reached about $109 with extra credit packs. Rebuilt in self-hosted n8n, it ran on a $12 VPS with unlimited executions, plus about two hours a month of maintenance we told the client to budget for. The full comparison has the numbers and the honest caveat: at a few hundred leads a month, managed and zero-maintenance is often the better deal.
Where it goes wrong
Duplicates in the CRM. The template upserts on email, so the same person twice is one record. If you swap the CRM node, keep the upsert or you'll have three copies of every contact within a week.
The website fetch fails. Bot protection, timeouts, a site that's just a PDF. The node is allowed to fail and the model is told the site couldn't be fetched, so the lead is scored on the form alone and the concern is recorded. Don't make the fetch mandatory.
Free-mail addresses treated as junk. A gmail address is a signal, not a verdict. The template flags it and lets the model weigh it against the message; a founder writing from a personal address with a specific problem is still a lead.
The model as the router. The moment you let the model return "tier: hot" and route on that, you've handed a business rule to a probabilistic system. Keep the thresholds in code, where you can change them on a Tuesday and see exactly what changed.
AI-written replies to prospects. Every template library does it. It's the one send that fails the arithmetic above, and it fails it by a factor of three. If you add it, add a reviewer.
Polling instead of a webhook. If your form tool can't call a webhook and you poll it every few minutes, each poll is an execution. Use the webhook, or poll rarely.
The same rules hold for every agent we build for you. The handoff article is the longer version of why the "Needs a human look" node exists and what your Slack message should contain.
What to require from whoever builds it
Whether that's us or anyone else, this is what you should see before a lead pipeline goes live.
The ideal-customer paragraph written down, with the two thresholds, and a note of who can change them.
The scoring call returning a fixed JSON schema, not free text, and a test of what happens when it returns nothing.
The tier decided in code from the thresholds, never by the model.
Every failure path ending at a person: incomplete forms, fetch failures, refusals, parse errors.
Nothing model-written sent to a prospect without a review step, and approvals per hour on the dashboard if there is one.
An upsert on email in the CRM, and a test with the same lead submitted twice.
Time from form submission to Slack alert measured on the first week of real leads, with the median and the worst case.
A month of overrides reviewed: every lead a rep re-tiered is a lesson for the ideal-customer paragraph.
Frequently Asked Questions
Does this need a paid enrichment API?
No. The template fetches the lead's own website and reads it, which is free and needs no key. A paid enrichment service drops in at the same node if you want firmographics, and the model is told what was and wasn't fetched either way.
Can I use OpenAI or another model instead?
Yes. The scoring call is a plain HTTP Request, so you change the URL, the headers and the body. Keep the JSON schema for the answer, or change the "Read the score" node to match, otherwise every lead goes to review.
Which CRM does it work with?
HubSpot out of the box, because the template uses its upsert. n8n has nodes for Pipedrive, Salesforce, Zoho and most others; the fields you need to map are name, email, company, website, score, tier and summary. A Google Sheet works as the CRM for a small team.
Why does the model use "low" effort?
Because scoring a form against a paragraph is a simple task, and the lower setting answers in about a second at the same accuracy for this kind of work. The reasoning still happens. It's shorter. If your ideal-customer rules are genuinely complex, raise it and measure the difference on your own leads.
What if the lead volume is tiny?
Then the clock still matters to you and the cost doesn't. At twenty leads a month the pipeline costs cents to run, and your Friday-afternoon lead still gets its acknowledgement and its Slack brief within a minute. Use managed n8n and forget about hosting. If you would rather have the whole thing built and documented for you, that is our n8n build and handover service.
Is my form data being sent anywhere else?
To your n8n instance, to the lead's own website (a GET request with nothing attached), to the model provider for scoring, and to your CRM, Slack and email. Nothing else. If the lead is in the EU, the acknowledgement is the natural place to say an automated system handled the first step.
Lead routing review
Want this running on your form by next week?
Send us the form, the CRM and the Slack channel, and we'll install the template, write your ideal-customer paragraph with you, and measure the first week's speed-to-lead, whether you keep us on afterwards or not.