How to Automate Lead Capture from Web Forms with n8n

By The LightCube Labs team

This guide shows how to turn any web form into a complete n8n lead capture pipeline: a webhook receives the submission, a Code node validates and sanitizes it, HubSpot stores it, the lead gets a confirmation email, and your sales channel gets an alert.

The Problem

Web forms often end in an inbox, which n8n can avoid. Someone copies the details into the CRM later, bad email addresses slip through, and the lead waits hours for any reply. Pasted HTML and spam entries pollute contact records, and different forms use different field names, so reports never line up. A single n8n workflow can sit behind every form, reject bad input, map clean fields into HubSpot, send a prompt confirmation and notify the team in Slack while the lead is still on your site.

What You Will Need

Step-by-Step Workflow Setup

1 Create the Webhook endpoint for form submissions

Every n8n lead capture workflow starts with the Webhook node. It gives your form builder a URL to post to, and it hands the submitted fields to the rest of the n8n workflow as JSON. Add the node in the n8n editor and configure:

Node: Webhook
HTTP Method: POST
Path: lead-capture
Authentication: Header Auth (optional but recommended)
Respond: Immediately
Response Code: 200
Options:
  Allowed Origins (CORS): https://www.yourcompany.com
  Raw Body: Off

The node shows two URLs. The Test URL only listens while you click Listen for Test Event in the editor; the Production URL works after you activate the workflow. Form builders must use the production URL. A typical production address looks like https://n8n.yourcompany.com/webhook/lead-capture.

Sample:
{
  "headers": { "content-type": "application/json" },
  "body": {
    "first_name": "Maria",
    "last_name": "Lopez",
    "email": " Maria.Lopez@Brightpath.io ",
    "phone": "+1 (415) 555-0147",
    "company": "Brightpath",
    "message": "We need a demo <b>this week</b>",
    "source": "pricing-page"
  }
}

In n8n, form data lives under $json.body, not at the top level. If your form builder sends URL encoded data instead of JSON, n8n still parses it into the same body object.

2 Validate and sanitize the input in a Code node

Public endpoints receive junk: empty fields, malformed emails, and HTML pasted into free text. An n8n Code node in Run Once for Each Item mode checks the required fields, normalizes values and escapes HTML entities before anything reaches your CRM. This is the main defense of the n8n workflow, so keep it first in line.

const b = $json.body || {};
const esc = (s) => String(s || '')
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  .replace(/"/g, '&quot;').replace(/'/g, '&#39;').trim();
const email = String(b.email || '').trim().toLowerCase();
const phone = String(b.phone || '').replace(/[^0-9+]/g, '');
const errors = [];
if (/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email) === false) errors.push('invalid_email');
if (phone.length > 0 && /^\+?[0-9]{7,15}$/.test(phone) === false) errors.push('invalid_phone');
if (esc(b.company) === '') errors.push('missing_company');
if (esc(b.first_name) === '') errors.push('missing_first_name');
return { json: {
  valid: errors.length === 0,
  errors,
  lead: {
    firstName: esc(b.first_name), lastName: esc(b.last_name),
    email, phone, company: esc(b.company),
    message: esc(b.message), source: esc(b.source) || 'unknown',
    submittedAt: new Date().toISOString()
  }
}};

The email pattern is intentionally simple: one at sign, a dot, and at least two letters at the end. Stricter patterns reject real addresses. For stronger protection, add a hidden honeypot field to the form and push any submission that fills it into the errors array.

Sample:
{
  "valid": true,
  "errors": [],
  "lead": {
    "firstName": "Maria", "lastName": "Lopez",
    "email": "maria.lopez@brightpath.io",
    "phone": "+14155550147",
    "company": "Brightpath",
    "message": "We need a demo &lt;b&gt;this week&lt;/b&gt;",
    "source": "pricing-page",
    "submittedAt": "2026-11-04T15:02:11.482Z"
  }
}

3 Branch on the validation result with an IF node

An n8n IF node sends valid submissions down the true branch and everything else down the false branch. Keeping the check in a separate node makes the n8n canvas easy to read and lets you attach different handling to each branch.

Node: IF
Conditions:
  Value 1: {{ $json.valid }}
  Operation: Boolean > is True

In n8n, connect the false branch to a Respond to Webhook node or to a Slack message that includes {{ $json.errors.join(', ') }}, so you can see why leads are failing in n8n. If your form is submitted from the browser, set the webhook node to Using Respond to Webhook Node and return a 400 with the error list so the form can show inline messages.

Sample:
{
  "valid": false,
  "errors": ["invalid_email", "missing_company"]
}

4 Create or update the HubSpot contact

On the valid branch, the n8n HubSpot node writes the lead into your CRM. The Create or Update operation matches on email, so repeat submissions update the existing contact instead of creating duplicates.

Node: HubSpot
Authentication: App Token
Resource: Contact
Operation: Create or Update
Email: {{ $json.lead.email }}
Additional Fields:
  First Name: {{ $json.lead.firstName }}
  Last Name: {{ $json.lead.lastName }}
  Company Name: {{ $json.lead.company }}
  Phone Number: {{ $json.lead.phone }}
  Lead Status: NEW
Custom Properties:
  lead_source: {{ $json.lead.source }}
  form_message: {{ $json.lead.message }}

Field mapping is where most n8n CRM problems start. HubSpot internal names are lower case with underscores, and dropdown properties only accept their stored values. Create lead_source and form_message as custom contact properties first, or the n8n HubSpot node returns a property does not exist error.

Sample:
{
  "id": "851",
  "properties": {
    "email": "maria.lopez@brightpath.io",
    "firstname": "Maria",
    "company": "Brightpath",
    "lead_source": "pricing-page",
    "hs_lead_status": "NEW"
  }
}

5 Send the confirmation email over SMTP

A short confirmation tells the lead that the message arrived and what happens next. Add an n8n Send Email node, which sends through any SMTP server you configure as a credential.

Node: Send Email
Credential: SMTP (host, port 587, user, password, STARTTLS)
From Email: Sales Team <sales@yourcompany.com>
To Email: {{ $('Validate and Sanitize').item.json.lead.email }}
Subject: Thanks, {{ $('Validate and Sanitize').item.json.lead.firstName }}. We received your request
Email Format: HTML
HTML: <p>Hi {{ $('Validate and Sanitize').item.json.lead.firstName }},</p>
<p>Thanks for reaching out. Here is what happens next:</p>
<ol><li>A specialist reviews your request within one business day.</li>
<li>You receive a reply with a proposed time to talk.</li></ol>
<p>The LightCube Labs team</p>

The expression references the Code node by name because the HubSpot node output replaces $json. Use values that were sanitized in step 2, so a malicious name cannot inject markup into the email. Avoid promises you cannot keep, such as a response time your team does not meet.

Sample:
{
  "accepted": ["maria.lopez@brightpath.io"],
  "rejected": [],
  "messageId": "<b2f1c9@yourcompany.com>"
}

6 Post the new lead to Slack

The last node tells the sales channel. Add an n8n Slack node after the email node, or run it in parallel from the HubSpot node so a mail failure never blocks the notification in n8n.

Node: Slack
Resource: Message
Operation: Send
Send Message To: Channel
Channel: new-leads
Text: New lead: *{{ $('Validate and Sanitize').item.json.lead.firstName }} {{ $('Validate and Sanitize').item.json.lead.lastName }}*
Company: {{ $('Validate and Sanitize').item.json.lead.company }}
Source: {{ $('Validate and Sanitize').item.json.lead.source }}
Time: {{ $('Validate and Sanitize').item.json.lead.submittedAt }}
Open in HubSpot: https://app.hubspot.com/contacts/YOUR_PORTAL_ID/contact/{{ $('HubSpot').item.json.id }}
Sample:
{
  "ok": true,
  "channel": "C09NEWLEAD",
  "ts": "1730732531.000300"
}

Replace YOUR_PORTAL_ID with the number in your HubSpot URL, then save the n8n node.

7 Connect the form builder and test end to end

Paste the n8n production webhook URL into your form builder. Most tools such as Typeform, Webflow, Tally and Gravity Forms have a webhook or integration setting that takes a URL and a POST method. Map the form fields to the names the Code node expects: first_name, last_name, email, phone, company, message and source.

Submit one valid test lead and one with a bad email. The first should create a HubSpot contact, send an email and post to Slack. The second should stop at the IF node. Check the n8n execution list to confirm both paths, then leave the n8n workflow active.

Troubleshooting

The n8n webhook returns 404 from the form builder

The form is probably posting to the n8n Test URL. That address only works while the editor listens for a test event. Copy the Production URL, activate the workflow and update the form. Also check that the method matches: the node accepts POST only when configured that way.

The body fields are undefined in the Code node

In n8n, form data sits under $json.body. If your builder sends nested keys such as fields.email, adjust the paths at the top of the Code node. Open the n8n Webhook execution once and inspect the exact structure before writing field names.

HubSpot rejects the contact with a property error

Custom properties must exist before you write to them, and dropdown fields accept only their internal values. Create lead_source and form_message in HubSpot settings first, then reference those internal names in the n8n HubSpot node. A 401 error means the private app token lacks the contacts write scope.

Free n8n Workflow Resources

Get started with the free workflow pack on the LightCube Labs templates page.

Production-Ready Workflow Packs

Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.

View the Starter ($29) and Pro ($99) Packs

Frequently Asked Questions

Should the validation run in the browser or in n8n?

Both. Browser checks give instant feedback to the visitor, but only server side checks in n8n are trustworthy, because anyone can post to a public webhook URL directly. Treat the n8n Code node as the source of truth.

How do I stop spam submissions from reaching HubSpot through n8n?

Add a honeypot field and a Header Auth credential on the n8n Webhook node, and reject entries that fill the honeypot or exceed a message length. For heavy abuse, put a CAPTCHA token check in the Code node before the IF.

Can I use a CRM other than HubSpot?

Yes. Replace the n8n HubSpot node with the node for your CRM, such as Pipedrive or Salesforce, and map the same cleaned fields. The validation, confirmation email and Slack steps stay as they are in n8n.

Why does n8n escape HTML if HubSpot already stores plain text?

Sanitized values also flow into the confirmation email and the Slack message, where raw markup could cause formatting problems or phishing text. Escaping once at the front keeps every later node safe.