n8n Lead Capture to Notion: Webhooks, Scoring, Slack Alerts
A tutorial for a lead pipeline in n8n: capture leads from a form webhook, Gmail and an n8n Form, classify emails with a local model, score each lead, store it in Notion and get a Slack reminder when a lead goes unanswered.
When leads arrive in the same inbox as newsletters, invoices and notifications, a contact form submission can sit unread for days. The fix is structural rather than a matter of checking email more often: give leads their own pipeline, with one place to store them, an alert in a channel people actually watch, and a reminder when nobody has replied.
This tutorial builds that pipeline in n8n with four parts:
- Capture leads from a website form, from email and from manual entry.
- Score each lead with a small Code node.
- Store it in a Notion database with a status.
- Alert on Slack immediately, and again each morning for leads still marked New after two days.
Every n8n node and parameter named here comes from the n8n documentation, linked where it is first used.
Part 1: capture#
Website form via the Webhook node#
Most form builders can POST each submission to a URL. In n8n, that URL comes from the Webhook node. The node shows two URLs:
- Test URL: registered when you select Execute Step or Execute Workflow; incoming data is displayed in the editor, which is what you want while mapping fields.
- Production URL: registered when you publish the workflow. Production data is not shown in the editor; open the workflow's Executions tab to inspect a run.
Set HTTP Method to POST. In Tally, for example, webhooks are configured from the form's Integrations tab, as described in Tally's webhook help page. Send one test submission to the Test URL, check which fields arrive, then switch the form to the Production URL after publishing.
Email via the Gmail Trigger and a classifier#
Not every enquiry comes through the form. The Gmail Trigger polls Gmail for new messages and can filter by label, by a Gmail search query, by read status and by sender. If you label enquiries yourself, the Label Names or IDs filter is enough. To catch them automatically, add a classifier after the trigger:
- In the Gmail Trigger, turn Simplify off. The documentation states that the simplified output contains message IDs, labels and headers (From, To, CC, BCC, Subject), not the body, so a classifier fed the simplified output has nothing to read.
- Add a Text Classifier node. Map the message text from the trigger's raw output into Input Prompt (inspect one execution to find the field that holds the body).
- Define two Categories, each with a name and a description:
lead("someone asking to hire or commission work") andother. The description is what tells the model what the category means. - Under When No Clear Match, choose Output on Extra, 'Other' Branch if you want to review unclear emails rather than drop them.
The Text Classifier is a cluster root node: the language model attaches to it as a sub-node. To keep emails on your own hardware, attach the Ollama Chat Model. If n8n runs in Docker and Ollama on the host, the base URL cannot be localhost; n8n's Ollama common issues page gives http://host.docker.internal:11434 for that set-up, and http://127.0.0.1:11434 when the error is connect ECONNREFUSED ::1:11434.
A model can misclassify, so treat this branch as a safety net next to the form, not a replacement for it.
Manual entry via an n8n Form#
Some channels have no usable API for this purpose, such as direct messages on social networks. The n8n Form Trigger serves a form at its own URL and starts the workflow when someone submits it. A short form (name, company, contact, what they want) bookmarked on a phone lets you push those leads into the same pipeline. The node's documentation also explains how to pre-fill fields with query parameters on the production URL.
Normalise the three sources#
Each source produces differently named fields. After each trigger, add an Edit Fields (Set) node in Manual Mapping mode that outputs the same shape (name, email, company, message, budget, timeline, projectType, source), then connect all three to the next step. Everything after this point reads only those fields.
Part 2: enrich and score#
Optional enrichment#
If you use a data-enrichment service, call it with the HTTP Request node using the lead's email or company domain, following that provider's API reference for the endpoint and authentication. Do not make the HTTP call from inside a Code node; the HTTP Request node handles credentials, query parameters and pagination options for you.
A scoring Code node#
Add a Code node in Run Once for Each Item mode. The weights below are an example to adapt; the budget and timeline values must match the options your own form offers:
const lead = $json;
let score = 0;
// Budget band chosen on the form
if (lead.budget === '5000+') score += 35;
else if (lead.budget === '2000-5000') score += 20;
else if (lead.budget === '1000-2000') score += 10;
// Type of work
if (lead.projectType === 'ongoing') score += 25;
else if (['new-website', 'redesign'].includes(lead.projectType)) score += 15;
// Urgency
if (lead.timeline === 'immediately') score += 15;
else if (lead.timeline === '1-month') score += 8;
// Penalise incomplete submissions
if (!lead.email) score -= 20;
if (!lead.message || lead.message.length < 50) score -= 10;
const priority = score >= 60 ? 'hot' : score >= 30 ? 'warm' : 'cold';
return { json: { ...lead, score, priority } };In this mode the code runs once per lead, $json is the current item, and the returned object becomes that item's output. n8n's data structure guide notes that the Code node adds the json key and the surrounding array automatically if you leave them out.
Part 3: store the lead in Notion#
Create a Notion database with properties such as Name (title), Email, Company, Source, Score (number), Priority (select: hot, warm, cold), Status (select: New, Contacted, In discussion, Proposal sent, Won, Lost), Budget, Timeline, Date received and Notes.
Then use the Notion node with Database Page → Create, mapping each property with expressions such as {{ $json.name }}, {{ $json.email }} and {{ $json.score }}, and setting Status to New. To append the original message as page content, the node's Block → Append After operation adds blocks to the new page.
To avoid duplicates when the same person submits twice, read the database first with Database Page → Get Many, check whether a page already has the same email, and route matches to Database Page → Update instead of Create, using an If node for the split.
Part 4: alerts and the follow-up reminder#
Immediate Slack alert#
Add the Slack node with Message → Send. Keep the text short enough to decide at a glance whether to reply now:
New lead ({{ $json.priority }}, score {{ $json.score }})
{{ $json.name }} – {{ $json.company || 'company unknown' }}
{{ $json.projectType }} | budget {{ $json.budget }} | {{ $json.timeline }}
"{{ ($json.message || '').slice(0, 200) }}"n8n expressions can contain JavaScript (see expressions for data transformation), so slice(0, 200) trims long messages. To send hot leads somewhere louder, put an If node on {{ $json.priority }} is equal to hot before two Slack nodes that post to different channels.
Daily reminder for unanswered leads#
A second workflow catches leads nobody has touched:
- Schedule Trigger on the Days interval, every 1 day, Trigger at Hour 9am. Set the workflow timezone, otherwise the node falls back to the instance timezone.
- Notion, Database Page → Get Many on the leads database, followed by an Edit Fields node that exposes
statusanddateReceived. - If node with two conditions joined by AND: String
{{ $json.status }}is equal toNew, and Date & Time{{ $json.dateReceived }}is before{{ $today.minus({ days: 2 }) }}. - On the true output, Slack Message → Send listing each lead's name, company and score.
$today and minus() come from n8n's Luxon support, described in Work with dates and times. Because each matching lead is a separate item, the Slack node posts one message per lead; to post a single digest instead, collect them first in a Code node set to Run Once for All Items.
Checks before relying on it#
- Submit the form, send a test email and fill in the n8n Form once each, then confirm three pages appear in Notion with the right Source.
- Publish both workflows. Schedule and webhook production triggers only run once the workflow is published.
- Open the Executions tab after the first real lead to confirm every node ran with the data you expected.
Adjacent Guides#
- n8n automation production workflows hub
- n8n Complete Beginner Guide 2026: Build Your First Automation Workflow
- 18 n8n Workflow Templates for Freelancers and Solopreneurs (2026)
- n8n AI Agent Workflows with Ollama: Run Local AI Automations in 2026
- How I Fixed My n8n Workflow That Was Failing Silently for Three Weeks
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
n8n Client Reporting Workflow: ClickUp, Toggl and Gmail
A step-by-step n8n workflow that reads active clients from Notion, pulls completed ClickUp tasks and Toggl time entries, builds an HTML summary and leaves a Gmail draft per client for review before sending.
Migrating from Zapier to n8n: Docker Setup and Zap Mapping
A practical Zapier-to-n8n migration path: what to audit first, the Docker Compose settings n8n documents for self-hosting, the localhost trap inside containers, and which n8n trigger and action nodes replace common Zaps.
n8n requested webhook is not registered fix
This n8n webhook error is usually not about your payload. It is almost always a test-vs-production URL mixup or an inactive workflow.


