← Tidelines/Guides

How to turn parts of your system prompt on and off

One agent, many contexts: voice and chat, free and enterprise, US and EU. Some rules only apply in some of them. The trick is not writing five prompts, it is writing one prompt where the right sections switch off. Here is how to do it so the switch actually switches.

by TypeGlish team6 min read#guides
On by phone. Off in chat.

TL;DR To make a block of your prompt appear only in some contexts, put it in a tag section and guard the tag: <voice_rules when=@{channel} is equal to voice>. A when= on a # heading is inert and always ships, which the checker blocks with structure/guard-on-heading. Then build the prompt with the input set both ways and read the artifact to prove the section really drops.

A single agent usually wears several hats. The voice line needs spoken numbers and no account read-backs; the chat widget does not. Enterprise customers get a section on their dedicated queue; free-tier customers must never see it. The wrong reflex is to fork the prompt into voice.tg and chat.tg and keep them in sync by hand, which lasts exactly until the first edit that lands in one and not the other. The right move is one prompt with conditional sections: blocks that are present or absent depending on an input the caller passes. TypeGlish has a construct for exactly this, and one very common way to get it subtly wrong.

§1The mistake: guarding a heading

The intuitive thing, once you know a section can be conditional, is to write the condition next to the section header. The header is a # heading, so you put the when= there. It reads perfectly. It also does nothing.

agent.tg✗ won't build
<$CONFIG>
  $REQUIRE variable channel: one of voice, chat
</$CONFIG>

# Constraints
- MUST greet the customer by name in the first message.

# Voice rules when=@{channel} is equal to voice
- MUST speak dollar amounts as words, not digits.
- NEVER read a full account number aloud.
The author meant "these two rules apply on voice calls only." What they wrote is a heading with a decorative when= and two rules that ship on every channel, chat included, where "speak amounts as words" is nonsense.
tg check - output
agent.tg:2:3   warn   structure/unused-import  Required variable "channel" is never used.
agent.tg:8:15  error  structure/guard-on-heading  A when= guard on a # heading is
  INERT - headings cannot carry guards, so this content would ALWAYS ship. Make the
  section a tag: <name when=...> ... </name>.

 1 file - 1 error, 1 warning, 0 info
Two diagnostics, and the second is the tell. The channel input is flagged as never used, because the guard you wrote does not read it. You thought you were branching on the channel; the compiler can see that nothing is.

§2The fix: guard a tag section

A guard belongs on a section boundary the compiler can turn off, and that boundary is an XML-style tag, not a Markdown heading. Wrap the conditional rules in a named tag and put the when= on the opening tag. One catch worth knowing up front: the moment one section is a tag, the file's structure model is tags, so every other section becomes a tag too (indent the body two spaces). That is the whole edit.

agent.tg (fixed)✓ compiles · A (94/100)
<$CONFIG>
  $REQUIRE variable channel: one of voice, chat
</$CONFIG>

<constraints>
  @@ greeting: the name confirms we pulled the right account
  - MUST greet the customer by name in the first message.
  @@ brevity: replies over four sentences get skimmed, not read
  - MUST reply in at most 4 sentences.
</constraints>

<voice_rules when=@{channel} is equal to voice>
  @@ spoken_money: a screen shows "$50", a phone has to say it
  - MUST speak dollar amounts as words, not digits.
  @@ no_readback: reading an account number aloud leaks it to the room
  - NEVER read a full account number aloud.
</voice_rules>
Now the guard is on a boundary the compiler owns. The constraints section always ships; voice_rules ships only when @{channel} is voice. The channel input is genuinely read now, so the unused-import warning is gone and the file scores an A.

§3Prove it turns off

A guard you cannot see working is a guard you do not trust. Do not eyeball it: build the prompt with the input set each way and read the two artifacts. The build artifact is exactly what a model-taking system receives, with the scaffolding stripped, so the diff is the proof.

tg build - channel=voice
<constraints>
- MUST greet the customer by name in the first message.
- MUST reply in at most 4 sentences.
</constraints>

<voice_rules>
- MUST speak dollar amounts as words, not digits.
- NEVER read a full account number aloud.
</voice_rules>
Built with --vars '{"channel":"voice"}'. The guard has done its job and stripped itself off the tag, and the voice rules are present.
tg build - channel=chat
<constraints>
- MUST greet the customer by name in the first message.
- MUST reply in at most 4 sentences.
</constraints>
Built with --vars '{"channel":"chat"}'. The voice_rules section is simply not there. This is the difference the inert heading in §1 could never make: on chat, the model never sees the voice rules at all.

§4Beyond channels: tiers, locales, and when not to

The same construct covers most "only sometimes" content. Gate an enterprise-only playbook on when=@{tier} is equal to enterprise; gate an EU data-handling notice on when=@{region} is equal to eu. Each is one declared input and one guarded tag, and each drops cleanly out of the builds where it does not belong. Two habits keep it honest: declare every input the guards read so a typo becomes a dangling reference instead of a silent false, and build both branches in CI so a section that stops compiling in one variant cannot hide behind the variant you happened to test.

Reach for a guarded section when a block of content is present-or-absent. When you are instead picking exactly one variant out of several, like a different closing line per channel, a $SWITCH on the input is the better tool, because it proves you covered every case: adding SMS to the channel set turns an uncovered switch into a build error rather than a silent gap. That tradeoff is the whole story of the prose-versus-$SWITCH bake-off, and the two techniques compose: a guarded section for the rules a channel adds, a switch for the ones it swaps.

Field note

Section guards and the structure/guard-on-heading check are part of the structure tier the checker runs on every file; the fixed prompt here is re-built by the CI guard on each commit, so the A and the two artifacts are facts, not screenshots. If you are writing voice-only rules like the ones above, the channel changes what a rule can even mean, which is the subject of the voice-agent teardown. And a guarded section pairs naturally with a tight scope: decide what the agent covers at all before you decide what it covers only sometimes, per how to keep your AI support agent on topic.

FAQCommon questions

How do I make part of a system prompt conditional?
Put the conditional content in its own tag section and guard the tag, not a heading: <voice_rules when=@{channel} is equal to voice> ... </voice_rules>. When you build the prompt for a given input, the section is included if the guard holds and dropped entirely if it does not. The guard reads a declared input like @{channel}, so the caller decides at runtime which sections ship.
Why is my when= guard being ignored?
Because it is on a # heading, and headings cannot carry guards. A heading with a when= is inert: the content under it always ships, whatever the condition says. TypeGlish blocks it with structure/guard-on-heading and tells you to make the block a tag section (<name when=...> ... </name>) instead. As a tell, the checker often also flags the input you thought you were gating on as an unused import, because nothing is actually reading it.
How do I show different instructions for voice, chat, and email?
For a whole block that only applies on one channel, wrap it in a guarded tag section keyed on the channel input. For choosing one line out of a set per channel, a $SWITCH on the channel input is usually cleaner, because it proves you covered every channel. Use a guarded section when the content is present-or-absent, and a switch when you are picking exactly one of several mutually exclusive variants.
How can I prove a prompt section is actually being dropped?
Build the prompt with the input set both ways and read the artifact. Run typeglish build with the guard's input set to a value that includes the section, then to a value that excludes it, and diff the two .txt files. The excluded build simply does not contain the section, so you are reading exactly what the model receives rather than trusting that the guard worked.
∿ washed up Jul 24, 2026 ∿