Lead Intake
A web form that sorts customer messages with AI
Companies get orders, complaints, questions and spam through their website. This n8n automation reads each message, sorts it by type and urgency, looks up the company by its ID number in the Czech business register and drafts a reply for the team to check. It runs on a server I rent in the EU, and you can try it now.
A live demo: write to a made-up company as a customer and see within seconds how n8n and Claude sort your message.
How it works
A visitor writes a message in a web form. n8n, an automation tool, checks it, looks the company up in ARES, the Czech public business register, when the visitor gives its ID number (IČO), and asks Claude Haiku, an AI model from Anthropic, to sort the message: its type and priority, a one-line summary and a draft reply. Claude has to answer in a fixed format (a JSON schema), so the workflow can check the answer. The result shows on the next screen and stays in a table in n8n for 7 days; a copy without the message text goes to two Google Sheets files. Along the way the workflow calls the public ARES REST API, Anthropic's Messages API, Google Sheets through a service account and, in the local build, Salesforce.
If Claude fails, answers in the wrong format or the daily cap of 150 AI calls is used up, fixed rules sort the message instead, and the cap also limits the cost. A separate error workflow catches failures, and if a Google Sheets write fails, the message is kept and an alert row appears in n8n. The demo runs in Docker on a rented server in the EU, behind an nginx reverse proxy that lets only the demo's own pages through to the internet.
Each decision also gets a receipt that says whether Claude or the rules sorted the message, with the type and priority but no message text. Every hour a scheduled job links the receipts of each finished day into a chain of SHA-256 hashes (digital fingerprints), so a later change to any receipt would show. The /overeni page recomputes that chain right in the browser.
117 unit tests run before every deployment. Twelve sample messages, including spam and a prompt injection (a message that tries to slip its own instructions to the AI), go through the workflow on a local copy of n8n, from the check and the ARES lookup to sorting by the fixed rules, with no AI call.
To measure the AI step, 40 made-up test messages with checked labels went through the workflow twice: once sorted by Claude Haiku, once by the fixed rules. Claude put all 40 in the right category and got both category and priority right for 36. The rules got the category right for 24, and category and priority together also for 24; they missed most spam, including a prompt injection they filed as an urgent complaint. One message cost about $0.0002. Claude Code wrote the test messages, to the same definitions the prompt uses, and the test ran once with no repeats, so real mail may score lower.
The local build also hides personal details before a message reaches Claude, with the personal-data filter from the Insurance Assistant project: Claude reads stand-ins such as [jméno A] instead of names, phone numbers and addresses. The public demo does not have the filter, so there the message goes to Claude Haiku as written. On five sets of made-up messages, each read only after that round's rules were final, the filter hid 74% of the details on the first set and 93.8% on the fifth. On 11 Nov the insurer filter's newest rules were copied in, and sets 1 to 5 gave the same results, message by message. A sixth set came from a different model, Gemma 4 12B, which saw neither the filter nor the earlier sets: 39 messages with 125 personal details and 44 business words that must stay readable, in Czech, English, German, Slovak and mixed. The filter hid 103 of the 125 (82.4%; 102 with the older rules) and wrongly hid 2 of the 44 business words. Nobody checked that set's labels by hand, and its messages are harder than sets 1 to 5, so it is a stricter outside check, not a drop.
In the local build each stored message also becomes a Lead, Salesforce's record of a possible customer, in a Developer Edition org, created by n8n's Salesforce node. The Lead holds the receipt number, the firm and its address from ARES, the source, a status and a rating from the priority, but never the name, e-mail or message text. If Salesforce refuses a Lead, n8n adds an alert row and the rest of the run goes on. A Workato recipe (Free plan) watches the same org and gives every Hot web Lead a follow-up task due the next day; Leads with another rating start no job.
The demo's logic is written in JavaScript and the scheduled job in Python. AI coding agents (Claude Code) write the code in my projects under my direction; the brief, workflow design, test criteria, review and sign-off are mine.
Limits and decisions
- The customer never gets AI text. Claude writes only a reply draft for the team to review. The local build's confirmation e-mail to the customer is fixed text that never quotes the message, so the form cannot relay spam.
- The Gmail branch is not tested end to end. The local build can store messages in Google Sheets and send the e-mails through Gmail. That branch is built and its report logic has unit tests, but it has not run end to end yet, and none of it is on the public server.
- Salesforce stays in the local build. On the server it would need a privacy-notice passage naming Salesforce as a processor, a 7-day clean-up of the Leads, and a sign-in with a certificate instead of OAuth, because the OAuth sign-in's browser redirect cannot pass nginx.
- The personal-data filter is local only. The public server runs without it, so there the message goes to Claude as written. The filter also lets details through: on the sixth set it missed, among others, a kidney problem, a Slovak word for a religion, membership of a craftsmen's union, a bank debt, an income with no cue word and a Slovak name written in lower case.
- No message text in Google Sheets. Google keeps deleted rows in a file's version history for as long as the file exists, so the 7-day clean-up could not remove a message there. The sheets get the category, priority and receipt number, not the visitor's words. The demo also uses a free Google account, which has no data processing agreement; a client would use Google Workspace with Google's DPA.
- No CAPTCHA yet. The public server has per-address rate limits in nginx and a daily cap on AI calls. A CAPTCHA, and a per-address limit on the local build's customer confirmations, are not built; a company going live would need them.
What's inside
- The public form asks for a message, an optional company ID (IČO) and consent, never a name or…
- Claude Haiku sorts each message in one Messages API call that must answer in a fixed JSON schema…
- The visitor's text sits inside <inquiry> tags, is marked as data in the prompt and has its own…
- Costs are capped at 150 AI calls a day.
- Only the demo's own pages are reachable from the internet
- Every stored message also gets a receipt with its number and time, Claude or the rules (and why)…
Built with: n8n, Claude API, Salesforce, Workato, JavaScript, Python, Docker, Claude Code
Previous project: Portfolio HQ · Next project: Insurance Assistant · All projects