Sorting website form enquiries with an LLM that can only answer from a list A developer built an n8n workflow that uses an LLM to sort website contact-form enquiries into a fixed list of category ids, rejecting any reply that isn't an exact id match and routing it to needs_human instead. The workflow trims, lowercases and strips quotes from the model's reply, treats API errors the same as invalid answers, and escapes curly braces so form text isn't interpreted as template syntax. Tested on n8n 1.123.87 with local stand-ins for the OpenAI and Google Sheets APIs, 17 checks passed, though the developer notes a real model, real sheet, authentication and dedupe remain untested. 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. php 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 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. 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. 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 https://zhangautomation.gumroad.com/l/cqtfxr , 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 https://zhangautomation.gumroad.com/l/ecvlr .