Built a client intake classifier with n8n — five categories, a retry layer, and a redesign I didn't plan for

I built a workflow that takes an incoming client intake form, classifies it into one of five categories using an LLM, then sends a category-specific reply email plus an internal notification and logs everything to a shared Sheet.

The first version was simpler than what ended up shipping. Here’s what changed and why.

The five categories, and the one that gave me trouble

The categories themselves were straightforward except for two: ambiguous and system_error.
Early on I was treating anything the model couldn’t confidently classify as system_error, which was wrong — that category should mean the pipeline failed, not that the input was unclear.
Those are different problems needing different handling. I split them apart and added a has_partial_intent flag so a form that’s vague but not empty doesn’t get treated the same as one that’s genuinely broken.

I rewrote the category definitions twice after real test failures, not because the categories were wrong but because the field ordering in the prompt was affecting how the model reasoned through them.
Putting the disambiguating criteria before the category list instead of after noticeably changed output quality. I don’t have a clean theoretical explanation for why order mattered as much as it did — just that it did, consistently, across test runs.

Retry logic

The classification step can fail (API hiccup, malformed response, etc.), so I built in 3 retry attempts, 5 seconds apart — n8n’s actual max spacing for this, not an arbitrary choice.
If all three fail, the workflow falls back gracefully instead of crashing the whole run.
This felt like the least glamorous part of the build and also the part I’m most glad I didn’t skip.

The redesign I didn’t plan for

Original design had the Sheets log and the email send happening off the same node in sequence. That ran into the pattern I keep running into on action nodes — specifically Gmail:
sending overwrites the item data flowing through it, so anything downstream that expected the original item is now working with whatever Gmail handed back instead.
I didn’t catch this until the Sheets log started showing send-confirmation data where the original form fields should have been.

Fix was to decouple them — branch before the Gmail send so the Sheets log gets a clean copy of the original item, independent of what the email node does to its own branch. Simple once I saw it. Took a while to see it.

OAuth setup, briefly

Also went through Google OAuth + Gmail API setup from scratch for this — redirect URI config, adding myself as a test user since the app isn’t verified, and separately enabling the Gmail API (which isn’t automatic just because OAuth is configured).
Nothing novel here, just the usual first-time friction of a multi-step setup where each piece can silently be the missing one.

One bug I hit and fixed, more on it separately

Ran into Basic LLM Chain + Structured Output Parser breaking with a “Failed to parse agent steps” error partway through. Fixed by updating n8n.
I’ll write that one up on its own since it’s a specific enough error that someone searching the exact text should be able to find a fix quickly, rather than buried in here.

Where it stands

Working end to end now. Still not fully sure the category boundaries are as clean as they could be — ambiguous in particular still catches some things I’d want handled differently on a second pass.
If anyone’s built something similar and hit the field-ordering effect on prompt reasoning, curious whether that’s a known thing or something I stumbled into.

"Full workflow JSON and notes: GitHub - TeSidrah/ai-intake-classifier: n8n workflow that classifies client intake submissions with Gemini and routes each one — tailored email, internal alert, and Sheets logging — with retry/fallback and category-boundary testing. · GitHub