Bake-off: typed inputs vs. free-text for tier-gated rules
Two agent prompts branch on the customer’s tier. One types the input as a closed set; the other leaves it a free-text string. They behave identically and score the same, until the day a value is misspelled.
TL;DR When an agent input has a known, finite set of values (a plan tier, a channel, a region), declare it as a typed domain (one of free, pro, enterprise), not a bare string. A free-text input lets a comparison against a misspelled value compile clean and silently never fire; a typed domain catches the impossible value at build time with structure/impossible-case.
Half the rules in a real support prompt are gated on some enumerable fact about the customer: their plan, their region, their channel, their language. You write a branch per value and move on. The question this bake-off asks is small and boring and decides whether a whole class of bug can ship: when you declared the input the branch reads, did you tell the compiler what values it can hold? Two prompts, same tiers, same behavior. One typed, one not. We fed both to the checker, then made the mistake every team eventually makes.
§1The two contenders
Both prompts do the same thing: give enterprise customers a dedicated success manager, everyone else email support. Contender one declares the tier as a plain string. Contender two declares it as a closed set of the three tiers that actually exist. Same branch, one word of difference in the declaration.
$REQUIRE variable tier: string # Role You are a support agent for Acme. # Instructions $IF @{tier} is equal to enterprise: Offer a dedicated success manager. $ELSE: Offer email support.
@{tier} can hold literally anything. The comparison against enterprise is just one string tested against another.$REQUIRE variable tier: one of free, pro, enterprise # Role You are a support agent for Acme. # Instructions $IF @{tier} is equal to enterprise: Offer a dedicated success manager. $ELSE: Offer email support.
one of free, pro, enterprise. Now the compiler knows the exact universe of values @{tier} can take, and every comparison against it is checked against that universe.§2Round one: a dead heat
On correct inputs there is nothing to choose between them. Both compile with zero errors. Both run the same branch on the same value. Score them and it stays level: each lands at B (84/100), identical facet for facet, because the behavior the model sees is byte-for-byte the same. If you only ever pass a spelled-right tier, the typed declaration bought you nothing you can measure today.
Which is exactly why the free-text version is the one most teams ship. It reads a hair simpler, it works in every demo, and the extra clause looks like ceremony. A bake-off scored on day one calls it a tie and sends everyone home. So, as with any honest bake-off, we let a little time pass and let a human touch the file.
A tie on the happy path is not a tie. It is a bet on nobody ever typing the value wrong.
§3Round two: someone fat-fingers a tier
A quarter later, someone adds a rule and writes the tier from memory. It comes out enterprize, with a z. It happens on renames too: product relabels a plan and half the references update. Watch what each prompt does with the exact same slip.
The free-text prompt takes it without a word. @{tier} is a string, enterprize is a valid string, and the comparison is legal, so the file compiles clean. It even still scores a B (84/100). The only thing wrong is that the condition can now never be true, so every enterprise customer silently falls through to the $ELSE and gets pointed at email support. There is no error, no warning, no red build. The bug is live and looks healthy.
$REQUIRE variable tier: string # Role You are a support agent for Acme. # Instructions $IF @{tier} is equal to enterprize: Offer a dedicated success manager. $ELSE: Offer email support.
enterprize is a perfectly good string. The check is green and the enterprise path is gone.The typed prompt refuses the same edit. Because the domain is declared, the compiler knows enterprize is not one of the values @{tier} can ever hold, so the comparison is provably always false. It calls the arm what it is, unreachable, and blocks the build.
$REQUIRE variable tier: one of free, pro, enterprise # Role You are a support agent for Acme. # Instructions $IF @{tier} is equal to enterprize: Offer a dedicated success manager. $ELSE: Offer email support.
enterprize an impossible value, not a rare one.tiers-typed.tg:7:1 error structure/impossible-case @{tier} is never enterprize (it's one of free, pro, enterprise). ✗ 1 file - 1 error, 0 warning, 0 info
§4The verdict: close the set and the typo can’t hide
The free-text input asks you to be perfect: spell every value right, forever, across every edit and every rename, on branches nobody re-tests. That holds until the one time it doesn’t, and when it fails it fails silently, on your highest-value customers, in the branch you were most sure of. The typed input asks for nothing but the one clause that names the set. In return it makes an impossible value a failed build instead of a dead branch in production.
The rule of thumb is simple: if you can list the values, list them. A plan tier, a channel, a region, a language, a priority level, are all closed sets wearing a string’s clothing. Type them and the compiler polices every comparison against them for free. Save the bare string for inputs that really are open, a customer’s message or their name, where there is no set to close.
A value you can enumerate is a type you haven’t declared yet.
This is the same instinct the multichannel bake-off rewards from the other direction: there, a closed domain forced you to cover every member; here, it forbids you from testing a member that doesn’t exist. Coverage and reachability are two halves of the same guarantee, and you get both the moment you tell the compiler what the values are. It is also the input half of treating a prompt as an interface, the argument we make in full in your system prompt is an API contract.
§5Common questions
- Should I use a typed enum or a free-text string for an agent input?
- Use a typed enum (one of free, pro, enterprise) whenever the input has a known, finite set of values. It costs one extra clause to declare and it lets the compiler catch a value that can never match, such as a typo or a renamed tier, with structure/impossible-case. Reserve a free-text string for inputs that are genuinely open, like a customer's message or name.
- Why does my agent's tier or plan logic silently do nothing?
- Usually a condition compares the input against a value that can never occur, so the branch is dead. With a free-text string the compiler can't know your set, so a comparison to a misspelled value simply never fires and the else branch runs forever. Declaring the input as a typed domain makes that impossible value a compile error instead of a silent no-op.
- What is structure/impossible-case in TypeGlish?
- It is the error TypeGlish raises when a condition tests a typed input against a value outside its declared domain, for example @{tier} is equal to gold when tier is one of free, pro, enterprise. The comparison can never be true, so the arm is unreachable, and the checker blocks the build rather than let a dead branch ship.
- Do typed inputs change how the prompt behaves at runtime?
- No. On correct values the typed and free-text versions are identical, and they score the same. The typed domain only adds a compile-time guarantee: the set of allowed values is closed, so any comparison against something outside it is caught before the model ever runs.
The reachability proof behind structure/impossible-case reads the typed domains introduced with $REQUIRE variable, the same machinery that powers $SWITCH exhaustiveness from TypeGlish 0.1.0. Declare the set once and both guarantees, coverage and reachability, come along for free.