No ID column
The output required a product ID; the export did not provide one. The row number would have been the obvious stand-in — and wrong after the next import. So the ID is derived from the normalised title.
A real catalogue export of a D2C shoe brand and a clear brief: customers type freely, the system finds the unambiguous variant or asks a targeted question. Only code may decide — a language model phrases the answer, nothing more.
Every input ends in exactly one case: match, size missing, colour missing or model ambiguous. The output is JSON with a fixed shape — the language model receives it finished and turns it into a sentence.
{
"type": "question",
"question_key": "missing_size",
"options": ["46", "48", "50", "52", "54"],
"context": {
"model": "Zustieg Hose Herren",
"color": "Oliv",
"matched_candidates": [ … ]
}
}
The brief was a workflow that maps free input to a variant — or asks a targeted question when model, colour or size is missing or several models fit. A language model was to phrase the answer but not make the decision.
So the entire logic — matching, decision, options — sits in a single code node, deterministic and reproducible. The language model receives only the finished result and cannot change it. The same file runs unchanged in the workflow and in the tests: exactly the code that answers is the code that is tested.
An n8n workflow with three code nodes, a language model for phrasing only, Google Sheets as history and a React dashboard showing result, images and history.
Checked up front: the title format “model – colour – size” held in all 890 rows without exception. So code splits the title on import — faster, free and reproducible.
The required output asked for a product ID; the export had none. The ID is derived from the normalised title — not from the row number, which shifts with the next import.
Each word of a model name is weighted by how rare it is in the catalogue. Among the well-supported models the most complete one wins; a generic word on its own leads to a follow-up question rather than a random hit.
Code nodes in the workflow have no packages by default. Similarity is a custom Levenshtein function of about 20 lines with a 0.75 threshold; run-together spellings are caught via word pairs.
The dashboard sends the session and the context recognised so far: model → colour → size. If an input names a different model along the way, the input wins.
Interface in German, English, Spanish and French; the language is passed to the model, which answers in it. Product data stays untranslated — colour names are proper names. Model words from all four languages are recognised.
match
missing_size
missing_color
ambiguous_model
all cases
If a check fails — or the model does not answer at all, for instance on an outage, a timeout or an exhausted quota — a fixed template answers in the chosen language. The answer is never empty, and the dashboard shows which source answered. The reason for the punctuation rule: the model in use counts its thinking tokens towards the output limit; with too tight a limit, the sentence broke off mid-word.
Real shop exports are rarely as clean as the format promises. These 8 points shaped the implementation — described without names from the original catalogue:
The output required a product ID; the export did not provide one. The row number would have been the obvious stand-in — and wrong after the next import. So the ID is derived from the normalised title.
Without normalisation these would have become two models where one is meant — and every request for it ambiguous. The import smooths whitespace before building models.
Singular and plural, lines with and without a year, women’s, men’s and kids’ lines sharing a stem. Solved with rarity weighting and the rule “most complete match wins” — the stem word on its own is ambiguous and leads to a follow-up question.
One model is named after a short, common word that is contained in longer words. A simple substring search would have found it everywhere. Solution: word-by-word comparison, and words up to four characters must match exactly.
Single sizes next to paired sizes such as “38/39”, plus accessories with no size. A single size hits the matching pair; if there is only one size, the follow-up question is skipped.
Base-colour synonyms bring both languages to a common denominator. If someone names a colour family that the chosen model only has in English-named shades, the prototype asks specifically about those shades instead of guessing.
Not every variant has its own image. The dashboard therefore shows images as soon as model or colour are known — not only at the full match.
According to the workflow log the cause was not the language model but logging to Google Sheets: the workflow wrote to the sheet first and answered afterwards, and the dashboard reloaded the whole history after every message until Google throttled. Since then the workflow answers first and logs afterwards, the history loads only on request, and writes use append because parallel writes were otherwise overwriting each other.
Figures from the prototype, not from a shop in operation.
The 26 test cases run against the real export: all four decision cases, typos, the data traps above, a session over three turns and a schema check of every output type. The response time was measured across the whole demo run on a local n8n instance, after the “answer first, log afterwards” change; before that it reached more than a minute.
Conversion, revenue, returns, support volume: the prototype was never in the shop, so these values do not exist — and without measurement they would be guesswork. Also not here: the brand’s name and anything that would make its catalogue recognisable.
The live demo on the offer page uses the same matcher on an invented outdoor catalogue with built-in traps — entirely in the browser and with fixed templates instead of a language model. Next to it you can see what the code decided.
The matcher is not tied to shoes. What is needed is a catalogue export with model, colour and size — as a fixed-price prototype, without live operation.
Automated assistant • Online