← Tidelines/Best practices

Bake-off: prose conditionals vs $SWITCH for multichannel agents

One agent, three channels: voice, chat, email. Two prompts encode the same per-channel rules, and both compile clean. The difference only shows up the day someone adds a fourth channel and forgets an arm.

by TypeGlish team6 min read#best-practices
Add a channel. One prompt notices.

Most CX agents run on more than one channel, and the channel changes the rules. A voice bot has to speak money as words (“nineteen dollars,” not “$19”); a chat or email reply wants digits. It’s a tiny rule, and there are two honest ways to write it. They behave identically today. We ran them through the checker to find out which one still protects you after six months of edits.

§1The two contenders

Contender one writes the branch as prose the model reads and weighs at runtime: a stack of IF the channel is X THEN … lines. Contender two declares the channel as a typed input and hands the branch to the compiler with a $SWITCH. Same behavior, two spellings. Here they are, side by side.

channels-prose.tg✓ compiles
# Role
You are a billing-support agent.

# Output
- IF the channel is voice THEN speak money amounts as words.
- IF the channel is chat THEN write money amounts as digits.
- IF the channel is email THEN write money amounts as digits.
Prose conditionals. The model reads all three lines and decides which applies. Nothing here says “channel” is a closed set of exactly these three.
channels-switch.tg✓ compiles
$REQUIRE variable channel: one of voice, chat, email

# Role
You are a billing-support agent.

# Output
$SWITCH ON @{channel}
  - voice:: Speak money amounts as words.
  - chat:: Write money amounts as digits.
  - email:: Write money amounts as digits.
A switch over a typed domain. The compiler evaluates it: the losing arms never reach the model, and the set of channels is declared, not implied.

§2Round one: a dead heat

On day one, there’s nothing to choose between them. Both compile with zero errors. Score them and it stays a tie: both land at C (71/100). The prose version is arguably easier to skim; the switch version carries a declaration you had to write. If this were the whole story, you’d pick on taste and move on.

It isn’t the whole story, because a prompt is not a thing you write once. It’s a thing your team edits for a year. The real question a bake-off should ask isn’t “which is nicer today,” it’s “which one fails safely when the requirements change.” So we changed the requirements.

§3Round two: product adds SMS

A quarter later, support turns on SMS. Someone updates the channel list and, being human, updates most of the rules that mention it. The prose prompt gets a new IF line in some edits and not others; the switch prompt gets a new member in its declared domain. Watch what each does when the SMS arm is the line that got missed.

The prose prompt doesn’t blink. Adding SMS to the world doesn’t change any line already in the file, so it compiles exactly as before, silently covering three of four channels. An SMS user hits the agent and gets whatever the model improvises, because no rule speaks for that branch.

channels-switch.tg✗ blocked
$REQUIRE variable channel: one of voice, chat, email, sms

# Role
You are a billing-support agent.

# Output
$SWITCH ON @{channel}
  - voice:: Speak money amounts as words.
  - chat:: Write money amounts as digits.
  - email:: Write money amounts as digits.
The domain grew to four members; the switch still has three arms. The compiler proves the gap and refuses to build.
tg check - output
channels-switch.tg:7:1  error  structure/non-exhaustive-switch
  @{channel} can be sms, but no arm handles it - add a
  "- <member>::" row for each (or a deliberate "- otherwise::" fallback).

 1 file - 1 error, 0 warning, 0 info
This is the whole bake-off in one line. The switch can’t be partially right about a closed set: cover every member or don’t compile.

§4The verdict: totality beats vigilance

The prose prompt asks you to be vigilant: every time the channel set changes, remember to audit every branch that reads it. That works right up until the one edit where you don’t. The switch prompt asks nothing of you. It makes coverage a property the compiler proves, so an incomplete branch is a failed build, not a production surprise on a channel nobody tested.

That’s the trade, stated plainly. A prose IF chain is the right tool when the conditions are open-ended, overlap, or lean on thresholds a compiler can’t see (“if the customer sounds frustrated”). A $SWITCH is the right tool the moment your branch is one-per-member of a set you can name: channel, tier, language, region. Those are exactly the axes CX agents fan out on, which is why the switch earns its keep here.

A closed set deserves a closed check. If you can list the cases, let the compiler count them.

Neither prompt is wrong. But only one of them turns “we forgot SMS” from an incident into a red build on the pull request that introduced it. In a bake-off scored on the day you ship, it’s a draw. Scored on the year you maintain, the switch wins going away.

Field note

The exhaustiveness proof behind structure/non-exhaustive-switch shipped with the $SWITCH engine in TypeGlish 0.1.0: add a member to a typed domain and every switch over it that lacks an arm fails loudly, all at once.

∿ washed up Jul 16, 2026 ∿