DEV Community

Sorting website form enquiries with an LLM that can only answer from a list

Contact forms collect everything: real sales leads, support questions, partnership pitches, job applications and a steady drip of spam. Someone has to read each one and decide where it goes.

An LLM can do that sorting, but if you let it answer freely you get categories like "Potential Sales Inquiry (High Value)" one day and "sales lead" the next, and your spreadsheet filters stop working. So I built a small n8n workflow where the model gets a fixed list of category ids and may answer with one of them or null. Anything else is treated as a failure.

How it runs

  • Form POST -> Webhook (name, email, company, message) -> Normalize; no valid email -> HTTP 400, nothing logged -> LLM sees: category ids + one-line descriptions, and the enquiry -> Code node: is the reply exactly one of my ids?
  • yes -> that category
  • no (null, unknown id, a sentence, API error) -> needs_human -> Append one row to Google Sheets -> Webhook answers 200 with the category

The category list lives in one Code node called Settings (edit me), something like sales_inquiry, support, partnership, job_application, spam, other, each with a short description and a priority. The prompt and the check both read from that node, so you can't end up with a prompt that mentions a category the check doesn't know.

The check is a few lines of code

The model's reply gets trimmed, lowercased, and stripped of quotes and a trailing full stop. "Partnership." therefore still counts as partnership.

After that it has to match an id exactly. A reply like "This looks like a partnership request" fails, even though it contains a valid id, because I don't want to guess what the model meant. It goes to needs_human with the reason written in a check_notes column. An HTTP 500 from the model API goes down the same path.

The webhook still answers 200 and the row is still written, so a flaky API doesn't lose enquiries.

One more thing that tripped me up in an earlier workflow: form text can contain {name} or {{ $json.x }}, and n8n's LangChain nodes treat braces as template syntax. The message needs escaping before it goes into the prompt. In this workflow, text with braces is stored in the sheet exactly as sent.

Testing

I ran it on a real n8n 1.123.87 instance with local stand-ins for the OpenAI and Google Sheets APIs, and 17 checks passed: correct categories logged for sales and support enquiries, form-encoded and JSON bodies, null / invented id / sentence / HTTP 500 all ending up as needs_human, braces surviving, missing or invalid emails getting a 400 with no row and no model call, exactly one row per valid request, and no other hosts contacted.

What I haven't tested: a real model and a real sheet. The checks show that a correct answer gets through and a bad one doesn't, not how well a given model sorts your enquiries. The webhook also has no authentication or dedupe, so add those before you put it on a public form.

Get it

This workflow is in a free bundle with two others (a WhatsApp FAQ bot that can only send stored answers, and a B2B cold-email drafter that never sends anything): 3 free n8n AI workflows, pay what you want, $0 is fine. If you need the WhatsApp side done properly, with signature checks, dedupe, opt-out and human handoff, the full kit is $19.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.