Case study · Prototype · E-commerce ·

Free text in, exact variant out —
on 890 real variants.

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.

Free text Matching & decision code · deterministic · tested Language model phrases Check layer Answer else: template
Origin
Technical assignment · not a client order
Catalogue
Real export of a D2C shoe brand · 890 variants
Stack
n8n · Gemini · Google Sheets · React
Status
Prototype · not in operation
Result · measured on the prototype

Four cases, exactly one decision per request.

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.

890 variants in the real catalogue
26 automated test cases
1.3–3.5 s per answer
4 languages · DE EN ES FR
{
  "type": "question",
  "question_key": "missing_size",
  "options": ["46", "48", "50", "52", "54"],
  "context": {
    "model": "Zustieg Hose Herren",
    "color": "Oliv",
    "matched_candidates": [ … ]
  }
}
Output shape, shortened — shown with the invented demo catalogue, same structure as in the prototype.
The brief

Logic in code, language in the model.

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.

Implementation

What was built.

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.

Catalogue split by code, not by a language model

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.

Stable IDs without an ID column

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.

Rare words count more

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.

Typos without libraries

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.

Several turns in one session

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.

Four languages

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.

A check layer instead of trust

after the language model
  • match
    The product title is named.
  • missing_size
    Model and colour are named; every number in the text is an allowed size.
  • missing_color
    The model is named.
  • ambiguous_model
    At least one of the model options offered is named.
  • all cases
    At most 600 characters, and the text ends with punctuation.

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.

Obstacles

What was hiding in the data.

Real shop exports are rarely as clean as the format promises. These 8 points shaped the implementation — described without names from the original catalogue:

01

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.

02

Trailing spaces on two model names

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.

03

Confusable model names

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.

04

A very short model word

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.

05

Two size systems — and items without a size

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.

06

Colours mixed in German and English

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.

07

Images only per model and colour

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.

08

Answers took more than a minute

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.

Measurable

What can be proven — and what cannot.

Figures from the prototype, not from a shop in operation.

26 /26
Test cases passed against the real catalogue
890
Variants from the real export
1.3–3.5 s
per answer after the latency fix
4
Decision cases one decision per request

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.

What is not here

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.

Try it yourself

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.

Go to the live demo

And your catalogue?

Same approach, your range.

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.

Alexander Ohl

Alexander Ohl

Automated assistant • Online

Hello! I am the Pragma-Code Assistant. How can I help you today? You can ask me about our services or select a topic below.