> Markdown rendition of https://typeglish.dev/blog/agent-replies-in-markdown ("How to stop your support agent replying in Markdown - Tidelines"). Canonical page: https://typeglish.dev/blog/agent-replies-in-markdown · All pages: https://typeglish.dev/llms.txt

[← Tidelines](https://typeglish.dev/blog)/ Guides Sep 25, 2026

# How to stop your support agent replying in Markdown

The reply said *Freeze card*, and the customer saw it wrapped in four asterisks. The rule that was supposed to stop it had been in the prompt since launch.

by **TypeGlish team** 7 min read #guides

Two stars. One word.

**TL;DR** If your chat widget shows raw text, `NEVER use Markdown` is a rule the checker reads and cannot prove, so it sits beside a rule demanding a bulleted list at `A (96/100)`. Name each mark instead, `NEVER include a bulleted list`, and the same file refuses to build until you decide which surface each rule belongs to.

Fernhill Bank runs one help assistant, Pip, on two surfaces. The web chat on the website renders Markdown. The widget inside the banking app does not: it prints whatever string arrives. So when a customer in the app asks how to freeze a card, they get a reply that opens with a star, a space, and *Open the Cards tab*, and further down two words wrapped in four asterisks. The prompt has a rule against this. It has had one since the app launch. Here is why it does nothing, and how to write the version that does.

## §1 Why the asterisks show up

Chat models format by default. Much of the chat they are built for renders Markdown, so a list of steps comes back as a bulleted list and a key phrase comes back in bold. On a surface that renders it, that is a feature. On a surface that does not, every mark arrives as punctuation: a star at the start of a line, two stars either side of a word, a hash in front of a heading, a pair of brackets and a URL in parentheses where a link should be.

Two things make this worse in support than elsewhere. The steps are the answer: a how-to question is exactly where a model reaches for a list, so the ugliest replies land on the most common intent. And the prompt usually came from somewhere that rendered fine. Fernhill's was written for the web chat, and it says so in one of its rules.

## §2 NEVER use Markdown is a rule about nothing

This is the formatting section as it stands, reduced to the two rules that matter. The first was written for the web chat. The second was added the week the app widget launched.

pip.tg - the web chat rule and the app launch rule ✗ nothing proven

```typeglish
# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Pip, the help assistant inside the Fernhill Bank app.

# Constraints
@@ steps: written for the web chat, which rendered Markdown
- WHEN listing steps THEN MUST include a bulleted list.
@@ no_markdown: the app widget shows raw text, so asterisks show up as asterisks
- NEVER use Markdown.
```

Every how-to question fires both rules. One asks for a bulleted list, the other forbids the format the list is written in. A person reading the pair sees the fight at once.

tg check and score - pip.tg ✗ A (96/100)

```
$ typeglish check pip.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ typeglish score pip.tg
pip.tg — A (96/100)  proven errors: none  tiers: base+z3
  planes  runtime 95 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 85 x.21 · hardness 100 x.12 · directness 95 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  lever   enforceability 85/100 (up to +3 overall) — 1 hard rule it cannot prove (L9): one action, one object, a catalog verb or a bound. The rules table below has the edit per line
  rules   2 of 2 rule-shaped lines read · 1 not provable
    L9   ~ unprovable    NEVER use Markdown.
                         the checker knows "use" but cannot prove this shape: "use" is a light verb with nothing bound to it → name the tool or object it acts on (call @[tool], make a note)
```

`consistency 100` on a file whose two rules disagree on every how-to question. The card is not wrong, it is honest about what it could see: line 9 is counted as a rule, and then set aside, because *use* is a light verb and *Markdown* is not a thing the reply can include or leave out.

That last point is the whole problem. Markdown is not an object in a reply, it is a family of marks. *Use Markdown* names no action anything can compare against *include a bulleted list*, so the prover has two rules about different things and no reason to object. The model, which does see that a bulleted list is Markdown, gets the conflict with no tiebreak, and resolves it however this conversation happens to lean. That is the flakiness from [your prompt argues with itself](https://typeglish.dev/blog/prompt-contradictions), with a formatting rule playing the part of the silent loser.

## §3 Name the marks, one rule each

Write down what the widget actually mangles. For Fernhill that is three things: a bulleted list, bold text, and a Markdown link. Each becomes its own rule with a real verb and one object.

pip.tg - the marks named ✗ logic/contradiction

```typeglish
# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Pip, the help assistant inside the Fernhill Bank app.

# Constraints
@@ steps: written for the web chat, which rendered Markdown
- WHEN listing steps THEN MUST include a bulleted list.
@@ no_bullets: the app widget shows a bullet as a bare asterisk
- NEVER include a bulleted list.
@@ no_bold: the app widget shows bold as two pairs of asterisks
- NEVER include bold text.
```

tg check - pip.tg, the marks named ✗ build blocked

```
$ typeglish check pip.tg
pip.tg:7:1  error  logic/contradiction  Conflicts with line 9. Logical conflict — "include a bulleted list." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
        ↳ line 9: - NEVER include a bulleted list.
pip.tg:9:1  error  logic/contradiction  Conflicts with line 7. Logical conflict — "include a bulleted list." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
        ↳ line 7: - WHEN listing steps THEN MUST include a bulleted list.

✗ 1 file — 2 error, 0 warning, 0 info
```

Nothing new was added to the conflict. It was there on the day the app launched. Naming the mark gave the second rule an object the prover could line up with the first, and the build now stops until somebody decides.

Two notes on choosing the marks. First, forbid the syntax, not the structure. A numbered step on its own line reads fine as plain text, so the widget rule is *put each step on its own line*, not *never give steps*. Second, only name marks your surface actually breaks. If the widget renders links but not bold, the link rule is noise. Open the widget, paste a reply full of Markdown, and write down what comes out wrong: that list is your rule list.

> Markdown is not a thing a reply contains. A bulleted list is.

## §4 One prompt, two surfaces

The contradiction offers two fixes: keep one rule, or scope them so they cannot both apply. Fernhill cannot keep one, because both surfaces are real. And the condition that separates them is not something the model should be guessing from the conversation. The host knows which surface it is serving before the first message, which makes it an input, not a guard. That is the line drawn in [every WHEN is a classifier](https://typeglish.dev/blog/every-when-is-a-classifier): a condition the host already knows belongs in a typed variable, where the losing arm never reaches the model.

pip.tg - formatting per surface ✓ A (97/100)

```typeglish
$REQUIRE variable surface: one of app_widget, web_chat

# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Pip, the help assistant for Fernhill Bank.

# Constraints
@@ brevity: both surfaces are read on a phone
- MUST keep every reply to at most 4 sentences.
@@ formatting: the host knows which surface renders Markdown, so the model never has to guess
$SWITCH ON @{surface}
  - app_widget::
    - WHEN listing steps THEN MUST put each step on its own line.
    - NEVER include a bulleted list.
    - NEVER include bold text.
    - NEVER include a Markdown link.
  - web_chat::
    - WHEN listing steps THEN MUST include a bulleted list.

$TEST widget_steps
  - input:: How do I freeze my card in the app?
  - expect::
    - puts each step on its own line
    - includes no bulleted list, no bold text and no Markdown link
    - matches /^[^*#\[]+$/
    - at most 4 sentences
```

The app launch rule and the web chat rule both survive, each in the arm for the surface it was written for. No build carries both.

tg check, score, test --dry and build - pip.tg per surface ✓ 6/6 rules exercised

```
$ typeglish check pip.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ typeglish score pip.tg
pip.tg — A (97/100)  proven errors: none  tiers: base+z3
  planes  runtime 96 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 88 x.21 · hardness 100 x.12 · directness 96 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  lever   enforceability 88/100 (up to +3 overall) — Lead every rule with a modal — MUST / NEVER / SHOULD — or put it after THEN; one action and one object per line, a verb the checker knows or a bound ("at most 3 sentences"). Descriptive detail with no bound does not move this facet. `typeglish score <file>` prints the rules table with the edit per line; `typeglish --explain enforceability <file>` prints just that table.
  rules   6 of 8 rule-shaped lines read · 6/6 covered by $TEST
    L12  · arm           app_widget::
                         a $SWITCH / $IF arm body — selected at compile time; the checker does not read arm bodies as rules
    L17  · arm           web_chat::
                         a $SWITCH / $IF arm body — selected at compile time; the checker does not read arm bodies as rules

$ typeglish test pip.tg --dry
✓ pip.tg  coverage: 6/6 rules exercised
  · widget_steps — "How do I freeze my card in the app?" (not run)
      ✓ matches /^[^*#\[]+$/
      ✓ at most 4 sentences
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed

$ typeglish build pip.tg --vars '{"surface":"app_widget"}'
✓ built .typeglish/dist/pip.txt ← pip.tg (b2c91a8411c6, full)

$ typeglish build pip.tg --vars '{"surface":"web_chat"}'
✓ built .typeglish/dist/pip.txt ← pip.tg (5553f607aa4e, full)
```

The `enforceability 88` is the arm headers being counted and set aside, which the rules table says in as many words. The two builds carry two different hashes, one per surface.

Here is what the app widget build actually sends:

.typeglish/dist/pip.txt - the app_widget build ✓ b2c91a8411c6

```
# Role
You are Pip, the help assistant for Fernhill Bank.

# Constraints
- MUST keep every reply to at most 4 sentences.
- WHEN listing steps THEN MUST put each step on its own line.
- NEVER include a bulleted list.
- NEVER include bold text.
- NEVER include a Markdown link.
```

The web chat rule is not in it at all. Look at the shape, too: a heading with a hash, rules on dash-led lines. The prompt that forbids Markdown is itself written in it. Whether that nudges your model is a question for your transcripts, not the file; the rules above are what the file can guarantee.

If this looks familiar, it is the same move as the per-channel prompt in [prose conditionals vs $SWITCH for multichannel agents](https://typeglish.dev/blog/channel-rules-bakeoff), and it buys the same insurance. The day somebody adds an email surface to the domain without an arm for it, `check` stops with `structure/non-exhaustive-switch`, *@{surface} can be email, but no arm handles it*, instead of shipping whichever formatting the model feels like.

## §5 Test the reply, not the rule

Every rule in the file is now provable and scoped, and none of that tells you what the model actually sends. The `$TEST` case is where the widget's real requirement lives: `matches /^[^*#\[]+$/` is a deterministic assert that the whole reply contains no asterisk, no hash, and no opening bracket. No judge, no rubric, no opinion. It fails on the first bold word.

The dry run validates that assert offline and reports that all six rules are exercised; a live run sends *How do I freeze my card in the app?* to the model and holds the reply to it. That regex is the one line of this whole file that checks the thing the customer sees, which is why it is worth writing before any of the rules. If your widget breaks backticks too, add one to the character class. If it renders links, take the bracket out. The assert is a description of your surface, one character at a time.

## §6 Common questions

**Why does my chatbot show asterisks in its replies?**

Because the model wrote Markdown and your chat widget does not render it. Chat models format lists and key phrases in Markdown by default, so a surface that prints raw text shows a bullet as a bare asterisk and bold as two pairs of asterisks around a word. The fix is on the prompt side: tell the model which marks your surface cannot show, one rule per mark, and test the reply for them.

**How do I stop an AI agent from using Markdown?**

Name the marks your surface breaks, one rule each: `NEVER include a bulleted list.`, `NEVER include bold text.`, `NEVER include a Markdown link.` Keep the structure you still want in a form plain text can carry, such as `WHEN listing steps THEN MUST put each step on its own line.` Then add a `$TEST` case with a deterministic assert, `matches /^[^*#\[]+$/`, so a live run fails on the first asterisk, hash, or bracket in a real reply.

**Does "Do not use Markdown" work in a system prompt?**

It reads as a rule and proves nothing. TypeGlish counts `DO NOT use Markdown.` and `NEVER use Markdown.` alike, then reports each as unprovable, because use is a light verb with nothing bound to it. Markdown is a family of marks rather than a thing a reply includes, so no other rule can be compared against it. A rule demanding a bulleted list can sit right beside it at `A (96/100)` with `consistency 100`, and the model settles the conflict alone.

**Can one prompt serve a chat surface that renders Markdown and one that does not?**

Yes, if the host tells the prompt which surface it is serving. Declare a typed input, `$REQUIRE variable surface: one of app_widget, web_chat`, and put each surface’s formatting rules in its own arm of a `$SWITCH ON @{surface}` block. Each build keeps one arm, so the widget build never carries the web chat’s bulleted list rule, and a new surface added without an arm fails with `structure/non-exhaustive-switch`.

Field note

Formatting rules are the ones teams are least likely to review, because they feel cosmetic, and the most likely to be inherited, because they were written for whatever surface came first. When a second surface arrives, the old rule rarely gets deleted; a new one gets appended beside it. That is the exact shape the prover is built for, as long as both rules name a thing. The cheapest audit you can run today: search your prompt for the word *Markdown*, and for its cousins, *formatting*, *plain*, *clean*, *simple*. Each one is a category standing in for a list of marks, and the list is the part the checker can hold you to.

∿ washed up Sep 25, 2026 ∿
