← Tidelines/Teardowns

Teardown: the IVR menu you pasted into the voice agent

The phone tree took a decade to get right, so the first draft of the voice agent prompt is the phone tree. Every option, every route, the safety line at the bottom. It imports clean, and it scores a C. The checker reads two of its rules, and the gas line is not one of them.

by TypeGlish team7 min read#teardowns
Press 1 for nothing.

TL;DR An IVR menu pasted into a voice agent prompt is a script for a keypad: the menu lines are prose the model may read aloud, the routing lines are If ..., sentences with no THEN that the checker cannot read as rules, and the safety line is one of them; rewrite each route as one WHEN ... THEN MUST rule, put safety first, and give every route its own test case.

Harbourline Energy is retiring its touch-tone menu. The new voice assistant, Remy, is supposed to let callers say what they need in their own words. The first prompt draft came from the people who knew the phone tree best, and it is the phone tree: the welcome, the menu, the routing table, a retry counter, and the one line every energy company has to get right. We imported it as-is.

§1The prompt, as pasted

ivr.tg - the phone tree, imported unchanged✗ typeglish/if-then
You are the voice assistant for Harbourline Energy. Thank you for calling Harbourline Energy.

Main menu:
Press 1 or say "billing" for billing and payments.
Press 2 or say "moving" if you are moving home.
Press 3 or say "outage" to report a power cut.
Press 0 to speak to an agent.

If the caller says billing, ask for their account number and transfer to the billing queue.
If the caller is moving home, transfer to the moves team.
If the caller reports a power cut, read the outage message and transfer to the outage line.
If the caller says agent or presses 0, transfer to an agent.
If the caller does not choose an option, repeat the menu.
After 3 failed attempts, transfer to an agent.
If the caller says they smell gas, tell them to hang up and call the gas emergency line.
Always be polite and keep it short.
tg review - ivr.tg✗ C (74/100), 2 of 2 rules read
$ typeglish review ivr.tg
ivr.tg — C (74/100) → A (97) projected if every fix below lands  check: 0 error, 1 warning, 9 info  tiers: base+z3

  FIXES  ranked by projected gain
  +12  put a "@@ why" note above L1 L3 L4 L5 L6 L7 L9 L10 +6 more — annotation 0 → 100
  +4   fix the 6 typeglish/if-then findings (L9 L10 L11 L12 L13 L15) — A conditional (IF / WHEN / UNLESS / WHILE / …) opened without its THEN — an ADVISORY by the owner’s ruling (2026-08,…
  +3   make L16 provable — one verb the checker knows, one object, or a bound (the rules table names each edit)
  +2   fix the 1 prompt/unmeasurable finding (L16) — A rule with no concrete, checkable action or bound.
  +1   give L16 an observable action or a bound ("at most 3 sentences")
  +1   fix the 2 prompt/unintroduced-definite findings (L9 L11) — A definite retrieves a card the document never introduces (Open the store. - which store?) - introduce the head on an…

  RULES  2 of 2 rule-shaped lines read · 1 not provable · 1 unmeasurable
    L16  ~ unmeasurable  Always be polite and keep it short.
                         no observable action or bound, so nothing can check compliance → name a concrete action or a bound ("at most 3 sentences")

  FINDINGS
  ivr.tg:7:1  info   prompt/unregistered-doer  A bare generic doer never enters the world model - instruction to the agent, or background about users? Use the imperative if the agent acts, or a definite party ("The user should ...") to register the doer.
  ivr.tg:9:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  ivr.tg:9:1  info   prompt/unintroduced-definite  "the billing queue" retrieves something this document never introduces - a model must guess which queue is meant. Mention a queue on an earlier line, or anchor it here ("their queue", "the queue you ...").
  ivr.tg:10:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  ivr.tg:11:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  ivr.tg:11:1  info   prompt/unintroduced-definite  "the outage message" retrieves something this document never introduces - a model must guess which message is meant. Mention a message on an earlier line, or anchor it here ("their message", "the message you ...").
  ivr.tg:12:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  ivr.tg:13:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  ivr.tg:15:1  info   typeglish/if-then  IF needs a THEN — write IF <condition> THEN <action>.
  ivr.tg:16:1  warn   prompt/unmeasurable  Unmeasurable rule — no observable action or bound, so nothing can check compliance. Name a concrete action ("cite the source", "at most 3 sentences") or move it to a prose block.

  TESTS  no $TEST cases — add one beside a rule; `test --dry` validates it offline
Sixteen lines. The rules table counts two rule-shaped lines and lists one, line 16, which it cannot measure. Nothing from lines 9 to 15 is in the table. Six of them get a typeglish/if-then note instead: an IF that never says THEN is a sentence, not a rule.

The import reported already clean: zero repairs, zero lifts, and that is accurate. Nothing in this file is malformed. It is a well-written document for a different machine, and the review is the first place that shows how little of it the checker can hold onto.

§2Lines 3 to 7: the menu has no keypad

The menu block is an instruction to the caller. Press 1 or say billing tells a human what to do with a handset. In a touch-tone system, a separate engine listened for the digit. In the voice agent there is no such engine unless you wired one, and the lines are now prose the model reads as part of its own instructions.

Two things follow. First, a model that has been handed a menu tends to recite it, so the agent you built to replace the menu opens by reading the menu. Second, the menu speaks to a different you. Line 1 says You are the voice assistant; line 5 says if you are moving home. Same pronoun, two people, which is the problem your prompt says it, the model picks one walks through. The checker's only note here is prompt/unregistered-doer on line 7: a bare doer nobody introduced.

The fix is not to rewrite the menu. It is to delete it and keep what it knew: which team handles what. That is a context sentence, not a list of options, and it lets the agent route from my bill looks wrong rather than from billing.

§3Lines 9 to 15: the routing table nobody can read

The routing lines are the heart of the prompt, and they are written the way people write routing in an email: If the caller says billing, ask for their account number and transfer. The comma does the work of THEN. The obvious repair is to add the THEN. Here are the billing line and the gas line, repaired exactly that far:

route.tg - the comma replaced with THEN✗ prompt/unparsed-rule
# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Remy, the voice assistant for Harbourline Energy.

# Constraints
@@ billing_route: the menu's option 1, given its THEN
- IF the caller says billing THEN ask for their account number and transfer to the billing queue.
@@ gas: the menu's safety line, given its THEN
- IF the caller says they smell gas THEN tell them to hang up and call the gas emergency line.
tg review - route.tg✗ 0 of 2 rules read
$ typeglish review route.tg
route.tg — B (81/100) → A (95) projected if every fix below lands  check: 0 error, 0 warning, 4 info  tiers: base+z3

  FIXES  ranked by projected gain
  +10  put the modal after THEN on L7 L9 — 2 rules become visible to the checker
  +4   fix the 2 prompt/unintroduced-definite findings (L7 L9) — A definite retrieves a card the document never introduces (Open the store. - which store?) - introduce the head on an…

  RULES  0 of 2 rule-shaped lines read
    L7   ✗ invisible     IF the caller says billing THEN ask for their account number and transfer to the billing queue.
                         the branch after THEN ("ask for their account number and transfe") has no modal, and the checker knows "ask" but cannot prove this shape: two objects or actions joined by "and" — it proves one at a time, so the line reads as prose → lead the branch with a modal (THEN MUST / NEVER / SHOULD …) and one object per line: split into two rules
    L9   ✗ invisible     IF the caller says they smell gas THEN tell them to hang up and call the gas emergency line.
                         the branch after THEN ("tell them to hang up and call the gas em") has no modal, and the checker knows "tell" but cannot prove this shape: two objects or actions joined by "and" — it proves one at a time, so the line reads as prose → lead the branch with a modal (THEN MUST / NEVER / SHOULD …) and one object per line: split into two rules

  FINDINGS
  route.tg:7:1  info   prompt/unparsed-rule  Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: the branch after THEN ("ask for their account number and transfe") has no modal, and the checker knows "ask" but cannot prove this shape: two objects or actions joined by "and" — it proves one at a time, so the line reads as prose. Fix: lead the branch with a modal (THEN MUST / NEVER / SHOULD …) and one object per line: split into two rules.
  route.tg:7:1  info   prompt/unintroduced-definite  "the billing queue" retrieves something this document never introduces - a model must guess which queue is meant. Mention a queue on an earlier line, or anchor it here ("their queue", "the queue you ...").
  route.tg:9:1  info   prompt/unparsed-rule  Reads like a rule, but the rule reader cannot parse it as one, so it earns no Enforceability credit and no measurability check: the branch after THEN ("tell them to hang up and call the gas em") has no modal, and the checker knows "tell" but cannot prove this shape: two objects or actions joined by "and" — it proves one at a time, so the line reads as prose. Fix: lead the branch with a modal (THEN MUST / NEVER / SHOULD …) and one object per line: split into two rules.
  route.tg:9:1  info   prompt/unintroduced-definite  "the gas emergency line" retrieves something this document never introduces - a model must guess which line is meant. Mention a line on an earlier line, or anchor it here ("their line", "the line you ...").

  TESTS  no $TEST cases — add one beside a rule; `test --dry` validates it offline
Adding THEN moved both lines from not a rule to invisible rule. Each branch has no modal and joins two actions with and, so the rule reader gives up and the line reads as prose. The table says what it needs: a modal after THEN, and one action per line.

Every routing line in a phone tree has this shape, because a menu option is a little procedure: collect something, then send the call somewhere. A rule is one act. So the billing route becomes two rules, one that asks for the account number and one that transfers once the caller has given it, and the transfer becomes a declared tool with a closed list of teams. A typo in a team name is then something the checker can see, not something a caller finds.

Line 13 and line 14 are a retry counter: repeat the menu, and after three failures, transfer. That is keypad logic. A caller talking to an agent does not fail to choose an option; they say something the agent has to understand or ask about. The counter goes. The route it protected, getting a person, stays as its own rule, and silence on the line is a separate problem with its own fix in Rewrite: nobody is on the other end.

§4Line 15: the gas line was never a rule

In the original file the most important instruction is line 15 of 16, after the retry counter and before the line about being polite. It has the same comma-for-THEN shape as the routes, so it gets the same typeglish/if-then note and the same absence from the rules table. The only line the rules table could show you was the politeness line, and it could not measure that one either.

The rewrite moves it to the top of the constraints and splits it into the two things it means. Tell the caller to call the emergency line. Never ask a caller in danger for an account number. The second rule matters because of a caller the menu never imagined: one who opens with a billing question and mentions the smell of gas halfway through the sentence. The checker does not report the overlap between asks about a bill and reports a smell of gas, because it has no way to know both can be true of one caller. It is the case the your router assumes one thing is wrong post is about, and the place it gets pinned is a test case with both in one sentence.

remy.tg - the routes as rules, safety first✓ A (99/100)
$TOOL transfer_call
  - description:: Transfer the caller to a Harbourline team.
  - input::
    - team:: one of billing, moves, outage, agent: the team that takes the call.

# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Remy, the voice assistant for Harbourline Energy.

# Context
@@ teams: what each team handles, so the agent routes from the caller's words instead of a menu
The billing team handles bills and payments. The moves team handles customers moving home. The outage team handles power cuts. The %gas emergency line% is 0800 111 999.

# Constraints
@@ gas_first: a gas smell is an emergency, and no routing question comes before it
- IF the caller reports a smell of gas THEN MUST tell the caller to call the gas emergency line.
@@ gas_no_id: nobody in danger should be asked for an account number
- IF the caller reports a smell of gas THEN NEVER ask for their account number.
@@ no_menu: the caller has no keypad to press and no menu to hear
- NEVER list menu options.
@@ billing_route: billing needs the account number before the transfer
- WHEN the caller asks about a bill THEN MUST ask for their account number.
@@ billing_transfer: the billing team takes it from there
- WHEN the caller has given their account number THEN MUST call @[transfer_call] with team billing.
@@ moves_route: moving home is its own team
- WHEN the caller is moving home THEN MUST call @[transfer_call] with team moves.
@@ outage_route: a power cut goes to the outage line
- WHEN the caller reports a power cut THEN MUST call @[transfer_call] with team outage.
@@ human_route: a caller who asks for a person gets one
- WHEN the caller asks for a person THEN MUST call @[transfer_call] with team agent.
@@ brevity: a voice turn is short
- MUST keep every reply to at most 2 sentences.

$TEST gas
  - input:: I can smell gas in the kitchen.
  - expect::
    - tells the caller to call the gas emergency line
    - does not ask for their account number

$TEST gas_and_bill
  - input:: My bill looks wrong, and I can smell gas in the hall.
  - expect::
    - tells the caller to call the gas emergency line
    - does not ask for their account number

$TEST no_menu
  - input:: Hi, what are my options?
  - expect::
    - does not list menu options
    - at most 2 sentences

$TEST bill
  - input:: I have a question about my last bill.
  - expect::
    - asks for their account number

$TEST bill_transfer
  - input:: My account number is 40021733, I want to query my bill.
  - expect::
    - calls transfer_call with team billing

$TEST moving
  - input:: I am moving house at the end of the month.
  - expect::
    - calls transfer_call with team moves

$TEST power_cut
  - input:: The power has gone off on our whole street.
  - expect::
    - calls transfer_call with team outage

$TEST person
  - input:: Can I just speak to a person, please?
  - expect::
    - calls transfer_call with team agent

§5The rewrite, and a coverage number to distrust

tg review, test --dry and build - remy.tg✓ 9 of 9 rules read, 9/9 covered
$ typeglish review remy.tg
remy.tg — A (99/100)  check: 0 error, 0 warning, 0 info  tiers: base+z3

  FIXES  none — every facet is essentially full

  RULES  9 of 9 rule-shaped lines read · 9/9 covered by $TEST

  TESTS  8 cases · 9/9 rules exercised

$ typeglish test remy.tg --dry
✓ remy.tg  coverage: 9/9 rules exercised
  · gas — "I can smell gas in the kitchen." (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · gas_and_bill — "My bill looks wrong, and I can smell gas in the hall." (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · no_menu — "Hi, what are my options?" (not run)
      ✓ at most 2 sentences
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · bill — "I have a question about my last bill." (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · bill_transfer — "My account number is 40021733, I want to query my bill." (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · moving — "I am moving house at the end of the month." (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · power_cut — "The power has gone off on our whole street." (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · person — "Can I just speak to a person, please?" (not run)
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed

$ typeglish build remy.tg
✓ built .typeglish/dist/remy.txt ← remy.tg (628c231b0fdd, full)
Nine rules, all read, all covered, and an artifact with no menu in it. The rules table went from two lines it could read to nine, and the one that matters most is first.

One thing worth knowing about that 9/9. A case covers a rule when its expect:: shares the rule's wording, and the match is by word stem. Before the four route cases at the bottom existed, the file already reported every rule covered, because the gas case expects that Remy tells the caller to call the gas emergency line, and call is also the verb in MUST call transfer_call. We checked: delete the gas expectations and coverage falls to 2 of 9. So the route cases are there because the routes needed testing, not because the number asked for them. Read the uncovered list when it appears, and read the cases when it does not.

What the rewrite does not do is decide what the gas safety message says. That wording belongs to your safety team, not to the prompt author, and if they hand you an exact script, it needs to reach the model as exact text; your quote marks never reach the model covers how to keep its edges through the build.

§6Common questions

Can I paste my IVR menu into a voice agent system prompt?
You can, but it will not do what the menu did. The press-1 lines are instructions to a caller with a keypad, so the model reads them as prose and often recites the menu back. The routing lines are usually written as If something, do something, with a comma where THEN should be, so the checker cannot read them as rules. Keep what the menu knew, which team handles what, as a context sentence, and write each route as one WHEN ... THEN MUST rule.
Why does TypeGlish say IF needs a THEN?
Because a conditional without its THEN is read as a sentence, not a rule. The typeglish/if-then finding marks a line that opens with IF, WHEN or UNLESS and never says THEN, which is how most routing tables and pasted policies are written. Adding THEN is the first step; the branch after it also needs a modal such as MUST or NEVER and a single action, or review reports the line as invisible.
How do I route calls to different teams from a voice agent prompt?
Declare one transfer tool with a closed list of teams, describe what each team handles in a context sentence, and write one rule per route: WHEN the caller reports a power cut THEN MUST call the transfer tool with team outage. If a route needs information first, such as an account number, make collecting it its own rule. Then give every route its own $TEST case, so coverage reflects the routes and not a shared word.
What should a voice agent do if the caller mentions a gas leak during another request?
Stop the other request and give the safety instruction first. Write it as the first constraint, IF the caller reports a smell of gas THEN MUST tell the caller to call the gas emergency line, add a rule that the agent never asks that caller for an account number, and test a message that contains both a billing question and the gas report. The checker cannot know two guards can be true at once, so the combined case belongs in the test suite.
Field note

A phone tree is the most honest documentation of your contact reasons you will ever get: someone had to decide every branch. That makes it a good input to a voice agent prompt and a bad copy of one. Run typeglish import on the menu as it stands, then read the rules table in typeglish review before you edit anything. The lines missing from that table are the ones the menu engine used to enforce for you, and each one needs to come back as a rule, a tool, or a test.

∿ washed up Oct 3, 2026 ∿