The point isn't logging โ it's speed
Any form plugin can dump a submission into a spreadsheet. That's not the hard part, and it's not the part that changes outcomes. The reason this pattern is worth building properly is speed-to-lead โ a lead that gets a human response in five minutes converts very differently from one that sits in an inbox until tomorrow. So the notification step isn't an afterthought bolted onto a logging workflow; it's the actual job.
Dedup before it hits the sheet, not after
Forms get double-submitted more often than you'd expect โ a nervous double-click, a retry after a slow page load. Checking for a duplicate (by email, or whatever the natural key is) before the write, not cleaning it up afterward, keeps the Sheet or CRM as a trustworthy source instead of something that needs manual dedup passes later.
Where this shows up across other builds
The same shape โ capture, dedup, log, notify โ is underneath a few other things I've built: a Sheets-backed customer database that auto-saves order details (email, name) from every purchase, and notification-on-new-row patterns for a few different Sheets. It's less "one specific project" and more the default answer whenever a client's actual problem is "I'm finding out about leads too late" rather than "I have nowhere to put them."
Sheets vs. a real CRM
Sheets is where most of these start โ it's free, everyone can read it, and n8n's Sheets nodes are reliable. It's also genuinely enough for a lot of small businesses. The workflow itself doesn't care much either way: swapping the write target from a Sheet to HubSpot or a similar CRM is a node swap, not a redesign, once the capture-dedup-notify shape is already right.
What two real versions of this look like
Both exports below live in my n8n instance. They are inactive demo builds of the pattern, not a client's production system, and I read the node settings straight from the workflow JSON.
- Version 1, six nodes. A Webhook (POST, path
contact-form) feeds a Set node that copiesname,emailandmessageout of$json.body. An IF node tests whetheremailis not empty. The true output goes to a Google Sheets append and to a Telegram alert; the false output sends a second Telegram message saying the lead left no email. - Version 2, seven nodes. The Webhook answers through a Respond to Webhook node. The IF node checks
body.emailagainst the regex^[^\s@]+@[^\s@]+\.[^\s@]+$. The Sheets append and the Telegram node both have retry on fail enabled and an error output. The visitor gets one of three JSON answers: 200ok, 400validation_error, or 500internal_error.
What the difference means in practice
Version 1 checks that the field is not empty, so abc passes. It also has no Respond to Webhook node, so the form gets the Webhook node's own immediate acknowledgement whether or not the email was valid, and the visitor never sees a validation error. Version 2 returns a real 400 for a bad address and a 500 if saving or notifying fails, which is what a form plugin needs in order to show a message.
What neither version does
Neither export contains a duplicate check. The dedup advice above is the pattern I recommend, not something these two workflows implement. The shape I would add is a Google Sheets read filtered on the email column before the append, followed by an IF on whether any row came back. I have not built that into either export, so treat it as a design suggestion rather than a tested fix.
When this pattern is the wrong tool
If the form or CRM you already use sends instant notifications and de-duplicates on its own, an extra webhook workflow only adds a place for things to break. The workflow earns its keep when the notification channel is one you actually watch, such as Telegram, and the source has no notification of its own.
n8n Automation Hub