An unrendered placeholder shipped in a live reply
In short
A placeholder variable not replaced in a support reply has 2 possible origins, and they need opposite fixes: the model produced template-looking text that was never a real variable, or the template engine ran and failed to substitute a variable that was. Establish which before changing anything, then put a validator in front of send that refuses any outbound message still containing delimiters.
Key takeaways
- Ask one question first: did the model write the braces, or did the engine fail to remove them?
- A model-invented placeholder never existed as a variable, so no substitution could ever fix it.
- Substitution must run after generation. Running it first leaves the model free to invent more.
- An empty value that renders as nothing is worse than a visible placeholder — nobody sees it.
- The only reliable stop is a validator that refuses to send text still containing delimiters.
"Hi {{first_name}}" went out to a customer. Before you look at the template engine, find out whether the model wrote those characters or your engine left them behind, because the fixes point in opposite directions. If the variable never existed, no substitution step could have resolved it and the work is in generation. If it did exist, generation is fine and the work is in rendering.
The distinguishing test takes a minute: look up the token in the list of variables your helpdesk actually supports. A placeholder that does not appear there was invented. One that does appear there had a value and did not get it.
Match the token against the variables that exist
| What leaked | Where it came from | Fix |
|---|---|---|
| A token your platform does not define | The model, imitating templates it saw in training replies | Corpus and instructions: stop the generator producing variable-shaped text |
| A real token, in the correct syntax | The engine did not run, or ran before the text existed | Pipeline order: substitute after generation, never before |
| A real token, wrong delimiters | The model reproduced a variable using another system's syntax | Normalise, or reject at validation — a near-miss is still a miss |
| A real token, split by markup | A rich-text editor inserted a tag or a zero-width character mid-token | Match on the rendered text, not the stored HTML |
| Nothing visible, a gap in the sentence | The value was empty and the fallback rendered nothing | Fail the send rather than rendering an empty string |
Cause 1: the model wrote a variable that was never a variable
Drafting systems learn from sent replies, and sent replies in most helpdesks were produced from macros containing merge fields. Depending on how the export was taken, the stored text may show the raw macro rather than the rendered output — so the corpus teaches the model that a reply opens with a greeting containing braces. It then writes braces. Nothing downstream can substitute them, because there is no variable to bind.
The tell is that the token is plausible but not real: a name nobody defined, or the right idea in the wrong syntax — double braces where your platform uses percent signs, a dotted path against a platform that uses flat names. All 3 corpus fixes apply, in the order they should always be tried: remove the raw-macro text from the corpus, exclude those documents at retrieval, and only then add an instruction.
Cause 2: substitution ran before the text it was meant to substitute into
In a pipeline where a macro is expanded first and the model then rewrites or extends the expanded text, the engine has already finished by the time the model produces its output. Any variable-shaped text the model adds afterwards is untouched by definition, and the failure is a sequencing decision rather than a bug in either component.
There is a legitimate reason teams build it this way — expanding first gives the model real values to work with, so it can write around the customer's actual plan instead of a token. Keep that benefit and close the gap by running substitution twice: once before generation to give the model real values, and once after, over the final text, as a normalisation pass. The second pass is what catches anything the model added.
Cause 3: the variable is real, and the record behind it is empty
A first-name field is populated for customers who signed up through your product and empty for the ones who emailed support directly, were created by an import, or came in through a shared mailbox where the address is a team alias. The engine resolves the variable correctly to an empty string, and the sentence arrives with a hole in it.
Check where that field is written before you fix it in the template. Contact records are frequently maintained by an integration rather than by the helpdesk, and a sync that pushes a blank over a populated value is a well-known failure of bidirectional integrations — the pattern described in how a two-way sync starts echoing itself. Patching the greeting hides a data problem that is also corrupting your reporting.
- Give every customer-visible variable an explicit fallback, chosen deliberately. "Hi there" is a decision; an empty string is an accident.
- Make the fallback a different string from the value, so you can count how often it fires. A fallback nobody measures is a data-quality problem nobody sees.
- Refuse the send when a variable in a legally or financially significant sentence is empty. A missing name is awkward; a missing amount or date is a different category.
- Treat an alias sender as a distinct case rather than a person with no name — the identity split argued across the helpdesk's record model.
Cause 4: the message was re-encoded on the way out
This is the one that survives every test because it only happens on the real send path. The compose box holds HTML; the outbound message is usually multipart, carrying an HTML part and a plain-text part; and the substitution may be applied to one and not the other. A customer reading in a client that prefers plain text sees the raw token while everyone testing internally sees the rendered version.
Encoding does the same damage more subtly. A rich-text editor that HTML-encodes a brace as its numeric entity — decimal 123 for an opening brace — leaves a token that looks correct to a human reading the rendered mail and matches no pattern the engine is looking for. The same is true of a zero-width character or a formatting tag inserted between the 2 opening braces when someone edited the middle of a token.
If the delimiter survived to the customer, something between your engine and the outbound message changed the bytes. Test the sent message, not the compose box.
Cause 5: nothing rendered, and nothing stopped the send
The worst version of this failure is invisible. A template that renders an empty string on a missing value produces "Your refund of will be processed" — grammatically broken, semantically empty, and impossible to find with a search for delimiters because there are none left. Nobody reports it, because the customer assumes a glitch and asks the question again.
Design the failure mode explicitly. For decorative variables, a fallback string is right. For anything carrying meaning — a figure, a date, an order reference — the correct behaviour is to block the send and tell the agent which value is missing. That is a rule about criticality, not about templating, and it has to be written down per variable rather than inferred.
Render, validate, then send — in that order, every time
- Generate the draft text, with no substitution applied to anything the model produced.
- Run substitution over the complete final message, including any signature, greeting or footer the template layer contributes. Everything the customer will receive has to pass through the same pass.
- Materialise every part that will actually be transmitted — the HTML part and the plain-text part — because these are the artefacts the validator must inspect. Validating the source template proves nothing about either.
- Scan each part for unresolved delimiters in every syntax your system has ever used, plus their HTML-encoded forms, plus any variable name your platform defines appearing as literal text.
- Block the send on a match, and show the agent the offending token and the part it appeared in. A warning that can be dismissed is a warning that will be dismissed on a busy queue.
- Log every block. A rising count is a corpus problem or an integration problem announcing itself early, and it is far cheaper to read than a customer complaint.
This validator belongs in the same pre-send position as the other automated checks, and building them as one pass is simpler than three. The entailment check described in the citation that points at an article saying otherwise runs on the same final text, at the same moment, on the same block-or-allow decision.
What a delimiter check will not catch
It will not notice a variable that resolved to the wrong value. "Hi Priya" addressed to Sanjay passes every check here, because the output is well-formed — that is an identity-resolution problem, not a rendering one. It also will not help with per-agent variables, which multiply the surface: every extra token a personalisation layer introduces is another that can go missing, which is one of the costs weighed in one house voice or every agent's own.
And it will not make a bad draft good. A long reply gives a placeholder more places to hide, and greetings sit at the top where a reviewer has already stopped reading closely — one more reason drafts that run twice as long as an agent would write are a risk and not only an irritation. The plumbing here — pipeline order, fallback policy, the pre-send validator — is internal tools and ops work in our SaaS and customer support practice, and it sits with the rest of drafted replies and agent assist.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why did a merge field not render in our support reply?
Either the token was never a real variable, or it was and the substitution step did not reach it. Check the token against the variable list your helpdesk defines: if it is not there, a model produced variable-shaped text and no engine could have resolved it. If it is there, look at pipeline order first — substitution running before generation cannot touch anything the model wrote afterwards.
Should a template render an empty string when a value is missing?
Only for decorative values, and only with a deliberate fallback. For anything carrying meaning — an amount, a date, an order reference — an empty render is worse than a visible placeholder, because a broken sentence reaches the customer and nobody can search for it afterwards. Block the send instead, and name the missing field to the agent.
Can we just tell the model not to write placeholders?
It helps, and it is the weakest of the fixes available. If your training corpus contains pre-substitution macro text, the model has learned that braces are part of the house style, and an instruction fights that on every generation. Re-export the corpus from the messages as actually sent, then keep the instruction as a backstop rather than the control.
Where should the pre-send check run?
On the fully rendered outbound message, immediately before transmission, and on every part that will be transmitted. Validating the draft in the compose box misses anything the template layer adds afterwards, and validating only the HTML part misses a plain-text alternative that was never substituted. Same text the customer gets, same bytes, same moment.
- drafted replies
- templating
- email delivery
- pre-send checks
The work behind this page
Builds from our portfolio that this page draws on.
Read next
- The draft promised a refund window that does not existA model asserting a commercial term nobody wrote is not a hallucination in general. It is one of 3 specific defects, and the first search you run tells you which.diagnostic
- Agents are deleting the same sentence out of every draftA repeated identical deletion is the cheapest diagnostic signal a drafting system produces. It says something is still emitting that sentence — and the prompt is the wrong place to stop it.diagnostic
- Draft acceptance rate: three outcomes it has to separateOne acceptance percentage hides the only signal worth having. Sent unchanged, sent after edits and discarded are three different verdicts, and the middle one carries almost all the information.definition
- Internal note: the half of a ticket the customer never readsEvery helpdesk has 2 compose boxes behind one text field. The internal note holds the reasoning, the doubt and the account context — and it is discoverable, which changes what belongs in it.definition
- A mail loop is manufacturing tickets between your system and theirsRunaway ticket volume from a single counterparty is rarely spam. It is two automated systems answering each other, and the fix is a header rule your responder should already have been applying.diagnostic
- Customer replies are opening new tickets instead of threadingOpen the raw source of one orphaned reply. Whether the References chain contains your outbound Message-ID decides whether this is a matching bug, a stripped-header problem, or an identity problem.diagnostic
Related across the site
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation