How to Build a Lead Routing Automation with n8n (No-Code Workflow)
Build a lead routing automation with n8n. Capture web leads, score by intent, route to CRM, and trigger automated nurture sequences. No coding needed.
Edward Chalupa
Founder, Whtnxt · Dallas, TX
Every week, I talk to business owners losing leads in the gap between their website form and their sales team. The lead fills out a form. Someone gets an email. Then the lead sits in an inbox while the sales person decides whether it is worth calling back. By the time anyone acts, the lead has gone cold or found a competitor. This is not a tool problem. It is a workflow problem.
A lead routing automation does one thing well: it takes a raw inquiry from any channel, attaches relevant context, scores it against your criteria, and puts it in the right place for the right person to act on. No manual triage. No spreadsheets. No “I’ll get to it after this call.” Unlike a high-level list of automation ideas, this is a specific, buildable system you can deploy today — I covered the strategic framework in the three automations every marketing team needs, and this post is the technical implementation for number two in that stack.
I have been running this exact architecture for client lead pipelines across multiple businesses. One of those is a self-hosted Twenty CRM instance that processes leads from three different web forms and routes them through n8n into separate pipelines. Another runs on a combination of NocoDB and Google Sheets for a client who was not ready to commit to a full CRM migration. Both use the same n8n workflow pattern with different output destinations.
This guide walks through the full system, from webhook capture to lead routing to CRM enrichment, with the exact configurations I use in production.
What You Are Building
A four-stage lead routing pipeline:
- Capture — A webhook receives lead data from your website form, Calendly booking, or any external source
- Score — An n8n function evaluates the lead against your qualification criteria and assigns a score and tier
- Route — A Switch node sends each lead to the correct destination based on its tier
- Act — Each destination performs the appropriate follow-up action: CRM record creation, notification, or nurture sequence
When it is running, a lead that submits a form on your website goes from form submission to an assigned CRM pipeline in under three seconds. No human touches it until a sales person picks up a hot lead from their queue.
The entire system uses standard n8n nodes. No custom code. No Python scripts. Just webhooks, function nodes, switch nodes, and CRM integrations.
What You Will Need
- n8n running — Self-hosted or cloud. The full n8n Docker deployment guide covers this.
- A web form — Any tool that fires webhooks on submission. Typeform, Tally, Formbricks, or a custom form all work.
- A CRM or database — Twenty CRM, HubSpot, NocoDB, or Google Sheets. Any platform with an API works.
- A notification channel — Telegram, Slack, or email.
Total setup: about 45 minutes if n8n is already running.
Stage 1: Capture Leads with a Webhook Node
Every lead routing workflow starts with a trigger. The most flexible trigger for this use case is the n8n Webhook node, which exposes a URL that your form or external system can POST data to.
Step 1.1: Create the Webhook Node
In a new n8n workflow, add a Webhook node with HTTP Method: POST, Path: lead-capture, and Respond: “On Last Node” so n8n waits until the full workflow finishes before returning a response.
Once you save and activate the workflow, n8n gives you a webhook URL that looks like:
https://your-n8n-instance.com/webhook/lead-capture
Step 1.2: Configure Your Form to POST to the Webhook
Every form platform handles webhooks differently. Here is the payload structure I expect at the webhook node:
{
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "214-555-0199",
"company": "Acme Construction",
"service": "HVAC",
"message": "Need a quote for commercial HVAC install",
"source": "website_form",
"utm_source": "google",
"utm_campaign": "hvac_dfw_q2"
}
Not all fields are required. The workflow is designed to handle partial data. If a lead only submits their name and email, the scoring stage simply assigns a lower base score and routes them to the nurture track.
For Formbricks, the webhook is under Settings > Webhooks. For Typeform, it is under Connect > Webhooks. For a custom form, set the form action to your n8n webhook URL. Expect 500 to 800 milliseconds for webhook delivery through a Cloudflare tunnel, which is fast enough for same-day lead response.
Stage 2: Enrich and Score the Lead
Before scoring, I run the lead through an enrichment check. This is optional but valuable for B2B pipelines where company signals affect priority.
Add a Function node after the Webhook node. In the JavaScript code block, I check for common data gaps and fill them from the available fields:
const lead = $input.item.json;
// Normalize email domain for company matching
if (lead.email && !lead.company) {
const domain = lead.email.split('@')[1];
lead.company_hint = domain;
}
// Set defaults for missing fields
lead.score = 0;
lead.tier = 'cold';
lead.timestamp = new Date().toISOString();
lead.score_breakdown = {};
return { json: lead };
This node does not do heavy lifting. It normalizes the incoming data structure so the scoring engine has predictable inputs.
Step 2.2: Build the Scoring Engine
The scoring logic lives in a second Function node. I use a weighted point system that evaluates four signal categories:
const lead = $input.item.json;
let score = 0;
const breakdown = {};
// Signal 1: Service type signals
const highIntentServices = ['commercial', 'emergency', 'install', 'repair'];
const service = (lead.service || '').toLowerCase();
breakdown.service_intent = 0;
for (const signal of highIntentServices) {
if (service.includes(signal)) {
breakdown.service_intent += 15;
score += 15;
}
}
// Signal 2: Message depth
const message = (lead.message || '').trim();
breakdown.message_depth = 0;
if (message.length > 50) {
breakdown.message_depth += 10;
score += 10;
}
if (message.length > 150) {
breakdown.message_depth += 10;
score += 10;
}
// Signal 3: Completed fields
breakdown.completeness = 0;
const fields = ['name', 'email', 'phone', 'company'];
for (const field of fields) {
if (lead[field] && lead[field].trim()) {
breakdown.completeness += 5;
score += 5;
}
}
// Signal 4: Source quality
breakdown.source_quality = 0;
const organicSources = ['google', 'organic', 'direct', 'referral'];
const paidSources = ['google_ads', 'facebook_ads', 'paid'];
const source = (lead.utm_source || lead.source || '').toLowerCase();
if (organicSources.includes(source)) {
breakdown.source_quality += 15;
score += 15;
} else if (paidSources.includes(source)) {
breakdown.source_quality += 5;
score += 5;
}
// Tier assignment
let tier = 'cold';
if (score >= 50) tier = 'hot';
else if (score >= 25) tier = 'warm';
lead.score = score;
lead.tier = tier;
lead.score_breakdown = breakdown;
return { json: lead };
The maximum possible score in this system is 100 points. A lead that submits a detailed commercial service request with a phone number, company name, and organic traffic source easily scores 50 or higher and routes to the hot pipeline. A lead that drops a single-line message with no company name scores around 15 and enters the nurture sequence.
Tune these weights against your actual conversion data after 60 to 90 days. What signals a hot lead for an HVAC company is different from what signals one for a SaaS product.
Step 2.3: Lead Decay
This pattern is not covered in other lead routing tutorials, and it matters more than the scoring formula itself. Leads submitted outside business hours should not trigger the same response as leads submitted during them.
I add a time-based decay check in a third Function node:
const lead = $input.item.json;
const now = new Date();
const hour = now.getHours();
const day = now.getDay();
let decay = 0;
// Weekend penalty: all leads score one tier lower
if (day === 0 || day === 6) {
decay = 20;
}
// After-hours penalty: leads submitted outside 8am-6pm get reduced priority
if (hour < 8 || hour > 18) {
decay += 10;
}
// Friday afternoon penalty: leads submitted after 2pm Friday
if (day === 5 && hour >= 14) {
decay += 15;
}
lead.decay = decay;
lead.adjusted_score = Math.max(0, lead.score - decay);
// Recalculate tier after decay
if (lead.adjusted_score >= 50) lead.tier = 'hot';
else if (lead.adjusted_score >= 25) lead.tier = 'warm';
else lead.tier = 'cold';
return { json: lead };
This prevents your sales team from seeing a queue full of “hot” leads that were submitted at 3:00 AM and have been sitting for six hours. A lead that scores 55 at 2:00 PM on a weekday is actually hot. A lead that scores 55 at 10:00 PM on Saturday needs to be contacted Monday morning, and the system should not wake anyone up for it.
Stage 3: Route the Lead by Tier
The routing stage uses n8n’s Switch node to send each lead to the correct path based on its tier field.
Step 3.1: Configure the Switch Node
Add a Switch node after the scoring function. Configure it to evaluate the tier field from the incoming JSON:
- Value 1:
hot— Routes to the high-priority pipeline - Value 2:
warm— Routes to the standard pipeline - Value 3:
cold— Routes to the nurture pipeline - Fallback: Any unexpected tier value routes to an error log
Each output connects to its own sub-workflow that performs the tier-appropriate action.
Step 3.2: Hot Lead Path — Instant Notification and CRM Record
For hot leads, I want two things to happen immediately:
- A Telegram notification fires to the sales team with the lead details and score breakdown
- A CRM record is created in Twenty CRM with the lead data and a priority flag
The Telegram notification uses an HTTP Request node pointed at the Telegram Bot API:
https://api.telegram.org/bot[YOUR_BOT_TOKEN]/sendMessage
The message body includes the lead name, service requested, score, and a direct link to the CRM record once created. I format it as structured text, not JSON, so it is readable in the Telegram notification.
For the CRM record, I use Twenty CRM’s GraphQL API through an HTTP Request node. Twenty exposes a create endpoint for any object type, including leads. The mutation looks like:
mutation CreateLead {
createLead(data: {
name: "{{ $json.name }}",
email: "{{ $json.email }}",
phone: "{{ $json.phone }}",
companyName: "{{ $json.company }}",
leadScore: {{ $json.adjusted_score }},
tier: "{{ $json.tier }}",
source: "{{ $json.source }}"
}) {
id
}
}
I wrote about the lessons from running Twenty CRM in production and this is one of the use cases where the self-hosted advantage shows. No rate limits. No API tier restrictions. The webhook to CRM record creation takes about 400 milliseconds on my setup. For simpler pipelines without a dedicated CRM, the routing workflow can output directly into a self-hosted NocoDB instance as a lightweight lead database.
Step 3.3: Warm Lead Path — CRM Record and Email Notification
Warm leads get a CRM record with a standard priority flag and an email notification to the sales team. No Telegram alert. No urgency. The lead goes into the same CRM pipeline but without the immediate push notification.
In the Switch node’s warm output, I use an HTTP Request node to create the CRM record with a priority: "standard" field instead of priority: "high". Then a Gmail node or SMTP node sends a daily digest-style email to the sales address with the warm leads batched from the last 24 hours.
Step 3.4: Cold Lead Path — Nurture Sequence
Cold leads do not need a sales call. They need education and time. I route cold leads into a nurture sequence.
The nurture sequence uses two destinations in parallel:
- A Google Sheets row is appended to a “Cold Leads” spreadsheet. This serves as a lightweight database I can query later to measure how many cold leads eventually convert.
- The lead is enrolled in a Listmonk email nurture sequence. Listmonk’s API accepts subscriber creation with custom attributes. I pass the lead’s service category and source so the nurture emails can be segmented.
The Listmonk API call uses an HTTP Request node with:
POST https://listmonk.yourdomain.com/api/subscribers
Content-Type: application/json
{
"email": "{{ $json.email }}",
"name": "{{ $json.name }}",
"lists": [3],
"attribs": {
"service": "{{ $json.service }}",
"source": "{{ $json.source }}",
"lead_score": "{{ $json.score }}"
}
}
List ID 3 in my setup is the cold lead nurture list. Your list ID will differ. The subscriber is created with a status of “enabled” so the first nurture email fires within the next campaign cycle.
Stage 4: Error Handling with a Dead Letter Queue
Every workflow fails eventually. A CRM API goes down. A Telegram bot token expires. A webhook payload arrives with an unexpected structure. When that happens, the lead should not disappear into a silent error.
I add a dead letter queue pattern to the workflow. Failed leads are written to a NocoDB table called “Failed Leads” with the raw payload and error message.
// In an error-triggered Function node:
const error = $input.item.json;
const failedLead = {
email: error.email || 'unknown',
error_message: error.error || JSON.stringify(error),
raw_payload: JSON.stringify(error),
timestamp: new Date().toISOString()
};
return { json: failedLead };
This writes to NocoDB via its REST API. I check this table once a week. In six months of running this workflow across three client pipelines, the dead letter queue has caught exactly two failures: one from a CRM API credential rotation I forgot to update, and one from a malformed webhook payload that a form update introduced.
Without the dead letter queue, those leads would have been lost without anyone noticing.
Connecting It All: The Complete Flow
End to end: a lead submits a form, your platform POSTs to the n8n webhook, enrichment normalizes the data, the scoring engine evaluates four signal categories, the decay node adjusts for timing, and the Switch node routes the lead. Hot leads trigger a Telegram alert and a high-priority CRM record. Warm leads enter the CRM with standard priority. Cold leads go to a Google Sheet and a Listmonk nurture sequence. Any failure is caught by the dead letter queue.
The whole pipeline runs in under three seconds. The only maintenance is a weekly check of the dead letter queue.
What to Tune First
After 30 days, adjust the scoring weights. Pull score breakdown data from converted hot leads and compare against cold leads. If every lead above 45 converted, lower the threshold. If no one below 60 converted, raise it.
Adjust decay penalties to match your team’s actual working hours. The same n8n workflow that routes the leads can produce a weekly scoring accuracy report — I covered the reporting workflow pattern in my client onboarding automation pipeline. A scheduled workflow that pulls lead data, compares scores to outcomes, and emails a summary turns a routing system into a continuously improving pipeline.