> Markdown rendition of https://typeglish.dev/blog/voice-agent-read-back ("How to write read-back rules for a voice agent - Tidelines"). Canonical page: https://typeglish.dev/blog/voice-agent-read-back · All pages: https://typeglish.dev/llms.txt

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

# How to write read-back rules for a voice agent

On a phone line the transcript is a guess. Speech recognition hears *fifteen* as *fifty* and a postcode as three different postcodes, and the only check a voice agent has is to say the detail back and let the caller correct it. The rules for that are short. The trap is that the verb you use for them is not the verb you think.

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

Confirm is a read-back.

**TL;DR** Write one guarded read-back rule per detail the agent will act on (`WHEN the caller gives a callback number THEN MUST read back the callback number`), never a blanket *read back every detail*, and forbid secrets with `NEVER read back`. TypeGlish treats *confirm*, *verify* and *double-check* as the same act as *read back*, so an ops rule saying `MUST verify the account number` is a blocking contradiction with a security rule saying `NEVER read back the account number`; read back a named partial instead.

Kestrel Energy is a fictional supplier with a phone line and a voice agent, Ada, that takes meter readings, callback numbers and account numbers all day. Every one of those is a string of digits spoken over a bad line, and every one of them goes somewhere that matters: into a bill, into an outbound call, into an account lookup. Here is how to write the rules that make Ada say them back, and the two places where those rules quietly argue with the rest of the prompt.

## §1 Read back the detail you are about to act on

A read-back is a checkpoint, not a courtesy. It belongs on the details that leave the conversation and do something: a number the callback system will dial, a reading that becomes a bill, a reference the agent will look up. It does not belong on everything the caller says, because a two-sentence reply that repeats the whole call back to the caller is the voice version of a wall of text.

So the shape is one rule per detail, guarded on the moment the caller gives it:

the rule shape

```
- WHEN the caller gives a callback number THEN MUST read back the callback number.
```

The tempting shortcut is one blanket rule. It reads well and it is exactly what a brief says: *always confirm the details back to the caller*. Here it is next to the security team's rule about card numbers.

blanket.tg - one rule for every detail ✓ clean

```typeglish
# Role
You are Ada, the phone agent for Kestrel Energy.

# Constraints
- MUST keep every reply to at most 2 sentences.
- MUST read back every detail.
- NEVER read back the card number.
```

Two rules that disagree about the card number the moment a caller reads one out.

tg check blanket.tg - output ✓ 0 error

```
$ npx typeglish check blanket.tg
blanket.tg:7:1  info   prompt/unintroduced-definite  "the card number" retrieves something this
  document never introduces - a model must guess which number is meant. Mention a number on an
  earlier line, or anchor it here ("their number", "the number you ...").

✓ 1 file — 0 error, 0 warning, 1 info
```

The only finding is a style note about which card. *Every detail* is never compared with *the card number*, so nothing says the blanket rule covers the one detail the other rule bans.

A person reading those two lines resolves it instantly: the card number is the exception. A model resolves it per call, and the checker does not resolve it at all, because a quantified object like *every detail* does not reach a specific one. Name each detail and the question never comes up. The card-number case in particular deserves its own treatment, which is the whole of [the customer just pasted their card number](https://typeglish.dev/blog/card-number-rewrite).

## §2 Confirm is a read-back

Now the collision that actually happens. The security team adds `NEVER read back the account number`, because the account number is what the IVR accepts as identification and the call may be on speaker in a shared office. Separately, somebody in operations copies a line from the agent runbook: *verify the account number*. They mean *look it up*.

collide.tg - an ops line and a security line ✗ logic/contradiction

```typeglish
# Role
You are Ada, the phone agent for Kestrel Energy.

# Constraints
- MUST keep every reply to at most 2 sentences.
- WHEN the caller gives a callback number THEN MUST read back the callback number.
- MUST verify the account number.
- NEVER read back the account number.
```

tg check and build - collide.tg ✗ nothing written

```
$ npx typeglish check collide.tg
collide.tg:7:1  error  logic/contradiction  Conflicts with line 8. Logical conflict — "verify the
  account number." vs "read back the account number." is both required and forbidden. Keep one,
  or scope the two rules so they cannot both apply (IF <condition> THEN ...).
            ↳ line 8: - NEVER read back the account number.
collide.tg:8:1  error  logic/contradiction  Conflicts with line 7. Logical conflict — "verify the
  account number." vs "read back the account number." is both required and forbidden. Keep one,
  or scope the two rules so they cannot both apply (IF <condition> THEN ...).
            ↳ line 7: - MUST verify the account number.

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

$ npx typeglish build collide.tg
typeglish build: collide.tg refused — nothing written
```

*Verify* and *read back* are different words, and the checker reports them as one act with opposite force.

That is not a false alarm. To a model on a phone line, *verify the account number* most naturally means *say it back and ask if it is right*, which is the thing security just banned. The checker's verb catalog treats *confirm*, *verify* and *double-check* as confirmation verbs and folds them onto *read back*, because in agent speech that is what they mean. It is worth knowing exactly where that family ends, so here is every verb tried against the same prohibition, one two-line file each:

MUST \<verb> the account number, against NEVER read back the account number

```
# verb           tg check
confirm          logic/contradiction
verify           logic/contradiction
double-check     logic/contradiction
repeat           0 error, 0 warning, 0 info
say              0 error, 0 warning, 0 info
state            0 error, 0 warning, 0 info
quote            0 error, 0 warning, 0 info
share            0 error, 0 warning, 0 info
check            0 error, 0 warning, 0 info
```

Three verbs fold onto *read back*. *Repeat*, *say*, *state*, *quote* and *share* do not, and neither does *check*, which the catalog files under tool work.

The table is also a guide to writing the prohibition. Turn it round, `MUST confirm the account number` against `NEVER <verb> the account number`, and only `NEVER read back` produces the contradiction; `NEVER repeat`, `NEVER say`, `NEVER state` and `NEVER share` all check clean against it. So write the security rule with *read back*, the one spelling that catches every *confirm* and *verify* somebody adds later, and write the lookup as what it is: a tool call, `WHEN the caller gives an account number THEN MUST call @[find_account]`. The same lesson, from the tool side, is [say escalate vs. call the escalation tool](https://typeglish.dev/blog/escalate-words-vs-tool).

## §3 Read back part of a secret, and name the part

Banning the full read-back leaves a real gap: a misheard account number now fails silently, into a lookup for somebody else's account. The usual fix on a phone desk is a partial read-back, the last four digits, which is enough to catch a mishearing and not enough to be worth overhearing. Written the obvious way, it checks clean.

partial.tg - the last four digits ✓ clean

```typeglish
# Role
You are Ada, the phone agent for Kestrel Energy.

# Constraints
- MUST read back the last 4 digits of the account number.
- NEVER read back the account number.
```

tg review partial.tg - output ~ 1 not provable

```
$ npx typeglish review partial.tg
partial.tg — B (84/100) → A (99) projected if every fix below lands  check: 0 error, 0 warning, 0 info

  FIXES  ranked by projected gain
  +12  put a "@@ why" note above L2 L5 L6 — annotation 0 → 100
  +3   make L5 provable — one verb the checker knows, one object, or a bound (the rules table names each edit)

  RULES  2 of 2 rule-shaped lines read · 1 not provable
    L5   ~ unprovable    MUST read back the last 4 digits of the account number.
                         the checker knows "read back" but cannot prove this shape: the object runs past six words → name the object in fewer words, or split the rule
```

The check is clean because the partial rule was never compared with anything. Its object runs past six words, so the prover sets it aside.

That clean result is the wrong kind of clean. It is not that the checker compared *the last 4 digits of the account number* with *the account number* and found them compatible; it could not read the first object at all. A rule the prover cannot hold is also a rule nothing will flag when the next edit contradicts it.

The fix the rules table asks for is a shorter object, and the honest way to get one is to give the partial a name. One declaration in `# Context`, *The account suffix is the last 4 digits of the account number*, tells the model what the suffix is and gives the rule a two-word object the prover can hold. The finished spec below does exactly that, and `WHEN the caller gives an account number THEN MUST read back the account suffix` reads as a full rule.

## §4 The spec, and the tests that listen for digits

Put together: one lookup tool, one read-back rule for each of the three details, a named partial, and a prohibition written with the verb that catches the rest. Three tests, one per detail, with deterministic asserts on the digits.

ada.tg - the read-back rules, finished ✓ A (99/100)

```typeglish
$TOOL find_account
  - description:: Look up a customer account by its account number.
  - input::
    - account_number:: string: the 8-digit account number the caller gives.

# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Ada, the phone agent for Kestrel Energy.

# Context
@@ suffix: a name for the partial, so the read-back rule stays provable
The account suffix is the last 4 digits of the account number.

# Constraints
@@ brevity: two sentences is about as much as a caller holds in their head
- MUST keep every reply to at most 2 sentences.
@@ lookup: the account comes from the system, never from memory
- WHEN the caller gives an account number THEN MUST call @[find_account].
@@ callback_readback: a misheard digit sends the callback to a stranger
- WHEN the caller gives a callback number THEN MUST read back the callback number.
@@ meter_readback: a misheard reading turns into a wrong bill
- WHEN the caller gives a meter reading THEN MUST read back their meter reading.
@@ suffix_readback: four digits are enough to catch a mishearing
- WHEN the caller gives an account number THEN MUST read back the account suffix.
@@ account_secret: the full number is a credential, and the call may be on speaker
- NEVER read back the account number.

$TEST callback
  - input:: You can call me back on 07700 900 123.
  - expect::
    - reads back the callback number
    - contains "07700 900 123"
    - at most 2 sentences

$TEST account
  - input:: My account number is 40718823.
  - expect::
    - reads back the account suffix
    - contains "8823"
    - matches /^(?!.*40718823)/

$TEST meter
  - input:: The meter says 04512.
  - expect::
    - reads back the meter reading
    - contains "04512"
```

The `account` test carries both halves: `contains "8823"` says the suffix was read back, and the negative lookahead `matches /^(?!.*40718823)/` fails the reply if the full number appears anywhere in it.

tg check, score and test --dry - ada.tg ✓ 6/6 rules exercised

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

$ npx typeglish score ada.tg
ada.tg — A (99/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 91 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   6 of 6 rule-shaped lines read · 6/6 covered by $TEST

$ npx typeglish test ada.tg --dry
✓ ada.tg  coverage: 6/6 rules exercised
  · callback — "You can call me back on 07700 900 123." (not run)
      ✓ contains "07700 900 123"
      ✓ at most 2 sentences
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · account — "My account number is 40718823." (not run)
      ✓ contains "8823"
      ✓ matches /^(?!.*40718823)/
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · meter — "The meter says 04512." (not run)
      ✓ contains "04512"
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed
```

Six of six rules read, none set aside as unprovable, and every rule named by a test. The asserts validate offline; the prose expectations wait for a live run with a key.

One practical note on the asserts. A test's `input::` is text, so it checks the rules, not the speech layer: it proves Ada reads the suffix back when the transcript says *40718823*, not that the transcript was right. That is the correct division of labour. The read-back exists precisely because the transcript can be wrong, and the caller, not the test suite, is the one who hears it. For the rest of what changes when the reply is spoken rather than read, see [a voice agent system prompt, line by line](https://typeglish.dev/blog/teardown-voice-agent).

## §5 Common questions

**How do I make a voice agent confirm details back to the caller?**

Write one guarded rule per detail the agent will act on, for example `WHEN the caller gives a callback number THEN MUST read back the callback number`, rather than one blanket rule. A blanket *read back every detail* is never compared with a specific prohibition such as `NEVER read back the card number`, so the two can disagree without any finding. Pin each rule with a `$TEST` whose asserts check for the digits, such as `contains "07700 900 123"`.

**Why does TypeGlish say verify the account number contradicts never read back the account number?**

Because the verb catalog treats *confirm*, *verify* and *double-check* as the same act as *read back*, which is how they read in agent speech. `MUST verify the account number` next to `NEVER read back the account number` is a blocking `logic/contradiction` and the build is refused. If you meant a lookup, write it as a tool call: `WHEN the caller gives an account number THEN MUST call @[find_account]`.

**Should my voice agent read back the full account number or card number?**

Read back a named partial instead. Declare it once, *The account suffix is the last 4 digits of the account number*, and write `MUST read back the account suffix` alongside `NEVER read back the account number`. Writing *the last 4 digits of the account number* directly in the rule checks clean, but only because the object runs past six words and the prover sets the rule aside as unprovable. A test with `matches /^(?!.*40718823)/` fails any reply that contains the full number.

**Which verb should a prohibition on reading details aloud use?**

Use `NEVER read back`. Against `MUST confirm the account number`, only `NEVER read back the account number` produces a contradiction; `NEVER repeat`, `NEVER say`, `NEVER state` and `NEVER share` all check clean. Writing the ban with *read back* means any later *confirm* or *verify* rule on the same detail is caught at check time.

Field note

Read-back rules tend to arrive in a voice prompt from three directions at once, and that is where the trouble comes from. Operations writes *confirm the details* because that is what the human agents were trained to do; security writes *never read out the account number* after an incident; the QA scorecard has a line about *verifying the caller*. Each is right, and nobody sees them together. The verb table in §2 is the useful artifact to take away: three words that mean *say it back* to the checker, and a handful that do not. It is the same kind of mismatch as [the tone is casual, your tone is formal](https://typeglish.dev/blog/two-owners-one-slot), two owners writing about one thing, except that here the thing is a number a stranger can overhear.

∿ washed up Sep 28, 2026 ∿
