← Tidelines/Guides

How to write change-of-address rules for an AI support agent

Changing an address looks like the easiest job you could give a support agent. It is also the move that sends someone else’s order to a stranger’s door. Four rules decide which of those your agent does, and the brief usually writes none of them clearly.

by TypeGlish team8 min read#guides
The new one, never the old one.

TL;DR Good change-of-address rules keep the account address and an order address apart, let your runtime decide who is verified with a guarded section, give the shipped parcel its own pair of rules, and read back only the new address, never the one on file.

An address change is a small request with a large blast radius. Get it right and a customer who moved house gets their next order. Get it wrong and the next order goes to whoever asked nicely, which is why people who take over accounts tend to change the delivery address first. The rules below are the four decisions a change-of-address flow has to make, each written so the checker can hold it.

§1The brief says their address, and means two things

Penrose Home sells homeware online. The brief for its chat agent’s address handling is four lines long, and it is a fair summary of what the team wants.

brief.tg - the address section, as briefed✓ C (77/100)
# Role
You are the chat assistant for Penrose Home, an online homeware shop.

# Addresses
- Customers can update their address in chat.
- Make sure you verify the customer first.
- Read the address back to confirm it.
- Orders that have shipped can't be changed.
tg review - brief.tg (rules table)✓ 2 of 4 rule-shaped lines read
$ typeglish review brief.tg
brief.tg - C (77/100) → A (99) projected if every fix below lands  check: 0 error, 0 warning, 4 info  tiers: base+z3

  RULES  2 of 4 rule-shaped lines read
    L5   ✗ invisible     Customers can update their address in chat.
                         no modal (MUST / NEVER / SHOULD) and "Customers" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L8   ✗ invisible     Orders that have shipped can't be changed.
                         no modal (MUST / NEVER / SHOULD) and "Orders" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
The permission and the shipped-order line are invisible to the checker. The two that read, verify first and read it back, read as rules the model decides for itself.

Before any wording fix, notice their address. A customer has at least two: the one saved on the account, which future orders use, and the one on each open order. “I’ve moved” means the first. “Can you send this one to my office” means the second, and changing the account address will not move that parcel. These are two jobs with two different tools, so they get two rules and two names. Introduce the account address once, in the role, so later rules can point at it:

the declaration and the two tools
The account address IS the address saved for future orders.

- WHEN the customer asks to change the account address THEN MUST call @[update_address].
- WHEN the customer asks to change the address on an order THEN MUST call @[update_order_address].
A fragment, so it is not marked for CI; the full file in §5 compiles with these lines in it.

§2Let the runtime decide who is verified

Make sure you verify the customer first reads as a rule, and that is the trap: the model is the one deciding whether verification happened. Write the obvious version and the checker points straight at the gap.

prose.tg - verification as two prose rules✓ B (81/100)
# Role
You are the chat assistant for Penrose Home, an online homeware shop.

# Addresses
- WHEN the customer asks to change their address THEN MUST call @[verify_customer].
- WHEN the customer is verified THEN MUST call @[update_address].

$TOOL update_address
  - description:: Change the address saved on the account.
  - input::
    - address:: the new address.
$TOOL verify_customer
  - description:: Send a one-time link to the email saved on the account.
  - input::
    - order_number:: an order on the account.
tg check - prose.tg✓ 0 error, 0 warning, 1 info
$ typeglish check prose.tg
prose.tg:6:1  info   prompt/agentless-passive  The doer is deleted (agentless passive) - who performs this? Name the actor or use the imperative: "Verify the order." instead of "The order is verified."

✓ 1 file - 0 error, 0 warning, 1 info
Who performs this? is exactly the right question. In this file the answer is the model, reading a chat in which anyone can type yes, I clicked the link.

Your runtime knows whether the one-time link was clicked. So make verification a typed input the runtime fills, and put the rules that change things in a section that only exists when it is true. A guarded section is decided at build time: the unverified build does not contain a rule that lets the agent call update_address at all. (Building an identity-verification gate, prompt-first builds the same gate with $SWITCH ON and a three-state input. A guarded section has one practical edge: the checker still reads the rules inside it. It does not read $SWITCH arm bodies as rules; it says so, “the checker does not read arm bodies as rules”, and enforceability drops with them.)

tg build --vars - the same source, two builds✓ one gate, two artifacts
$ typeglish build final.tg --vars '{"verified":true}'
✓ built .typeglish/dist/final.txt ← final.tg (0d570263090c, full)
<verified_changes>
- WHEN the customer asks to change the account address THEN MUST call update_address.
- WHEN the customer asks to change the address on an order THEN MUST call update_order_address.
- WHEN the order has left the warehouse THEN NEVER call update_order_address.
- WHEN the order has left the warehouse THEN MUST call redirect_parcel.
</verified_changes>

$ typeglish build final.tg --vars '{"verified":false}'
✓ built .typeglish/dist/final.txt ← final.tg (4d9af7a55704, full)
<verification>
- WHEN the customer asks to change an address THEN MUST call verify_customer.
- NEVER call update_address.
- NEVER call update_order_address.
</verification>
Excerpts of the two artifacts; the role and the read-back section are identical in both. Different hashes, because the model gets a different prompt. Nothing a customer types can move them from one to the other.

Two details are worth copying. The unverified section has a bare NEVER call @[update_address] while the verified section has a MUST on the same tool, and the checker does not call it a contradiction, because the two guards can never both hold. And the unverified build still carries the prohibitions, not just a missing permission: if your platform hands the model every tool regardless of the prompt, the NEVER is what stands between the tool and the impostor. If you can, withhold the tool in that state as well.

§3The parcel that already left

Orders that have shipped can’t be changed is true, and it gives the agent nothing to do. The customer still wants the parcel at their office. Once the courier has it, the warehouse tool cannot change it, but the courier may be able to redirect it. So the shipped case is a pair: a prohibition on the tool that cannot work, and an action that might.

the shipped case, inside the verified section
- WHEN the order has left the warehouse THEN NEVER call @[update_order_address].
- WHEN the order has left the warehouse THEN MUST call @[redirect_parcel].
Has left the warehouse, not is dispatched: the passive form drew the same prompt/agentless-passive note as §2. Inside the verified section, because an unverified chat cannot change anything anyway.

Be honest about what the checker proves here. WHEN the customer asks to change the address on an order THEN MUST call and WHEN the order has left the warehouse THEN NEVER call the same tool can both be true of one message, and the checker does not report it: the two conditions are worded differently, and only the model can tell that one message meets both. So the overlap gets two other defences. The tool itself should refuse a dispatched order, because a rule is a request and a tool check is a fact. And a $TEST sends the exact message that sits in the overlap, a parcel that went out this morning, and expects the redirect.

§4Read back the new one, never the current one

Reading a new address back catches the typo before it is saved: the wrong house number, a postcode with two letters swapped. (How to write read-back rules for a voice agent covers the general case on the phone.) But there is a second address in the conversation, the one on file, and reading that one out tells whoever is typing where the account holder lives. Most teams write both instincts down, and the obvious wording collides.

b2.tg - one address, two opinions✗ logic/contradiction
# Role
You are the chat assistant for Penrose Home, an online homeware shop.

# Confirming
- WHEN the customer gives a new address THEN MUST read back the address.
- NEVER read back the address.
tg check - b2.tg✗ 2 error
$ typeglish check b2.tg
b2.tg:5:1  error  logic/contradiction  Conflicts with line 6. Logical conflict - "read back the address." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
       ↳ line 6: - NEVER read back the address.
b2.tg:6:1  error  logic/contradiction  Conflicts with line 5. Logical conflict - "read back the address." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
       ↳ line 5: - WHEN the customer gives a new address THEN MUST read back the address.

✗ 1 file - 2 error, 0 warning, 0 info
The author meant two addresses. The file says one, so a bare NEVER covers the case the WHEN requires. The model would have settled this per chat.

The fix is not a guard. It is naming the two objects apart, so the rules are about different things:

b3.tg - the new address and the current address✓ 0 error, 0 warning, 0 info
# Role
You are the chat assistant for Penrose Home, an online homeware shop.

# Confirming
- WHEN the customer gives a new address THEN MUST read back the new address.
- NEVER read back the current address.
The adjective is what the checker keys on: the new address and the current address are different objects, so the pair is consistent. Keep both names whole. Drop both adjectives and the contradiction above comes back; drop only one, say NEVER read back the address beside MUST read back the new address, and the check stays green although the bare rule now forbids the read-back. A $TEST holds what the prover lets through.

§5The whole section, with its tests

All four decisions in one file: two addresses, a runtime-owned gate, the shipped pair, and the read-back pair. One test per risk.

final.tg - change of address, end to end✓ A (99/100)
$REQUIRE variable verified: boolean

<role>
  @@ role: the desk this file speaks for
  You are the chat assistant for Penrose Home, an online homeware shop.
  @@ account_address: two addresses, two tools; the account one is introduced so rules can name it
  The account address IS the address saved for future orders.
</role>

<verified_changes when=@{verified}>
  @@ account_change: only reachable once the host has seen the one-time link clicked
  - WHEN the customer asks to change the account address THEN MUST call @[update_address].
  @@ order_change: one order, not the account
  - WHEN the customer asks to change the address on an order THEN MUST call @[update_order_address].
  @@ in_transit: the warehouse cannot change a parcel the courier already has
  - WHEN the order has left the warehouse THEN NEVER call @[update_order_address].
  @@ redirect: the courier may still redirect it
  - WHEN the order has left the warehouse THEN MUST call @[redirect_parcel].
</verified_changes>

<verification when=not @{verified}>
  @@ verify_first: an unverified chat gets the link, never the change
  - WHEN the customer asks to change an address THEN MUST call @[verify_customer].
  @@ no_account_change: the tool stays out of reach until the host flips the flag
  - NEVER call @[update_address].
  @@ no_order_change: the same for a single order
  - NEVER call @[update_order_address].
</verification>

<confirming>
  @@ read_new: reading the new address back catches a typo before it is saved
  - WHEN the customer gives a new address THEN MUST read back the new address.
  @@ hide_current: whoever is typing may not be the account holder
  - NEVER read back the current address.
</confirming>

$TOOL update_address
  - description:: Change the address saved on the account.
  - input::
    - address:: the new address.
$TOOL update_order_address
  - description:: Change the delivery address on one order that has not left the warehouse.
  - input::
    - order_number:: the order to change.
    - address:: the new address.
$TOOL redirect_parcel
  - description:: Ask the courier to redirect a dispatched parcel, if the courier allows it.
  - input::
    - order_number:: the dispatched order.
    - address:: the new address.
$TOOL verify_customer
  - description:: Send a one-time link to the email saved on the account.
  - input::
    - order_number:: an order on the account.

$TEST new_flat
  - input:: I have moved, please change my address to Flat 4, 18 Mill Lane, Leeds LS6 2AB.
  - expect::
    - reads back the new address
    - contains "Mill Lane"

$TEST whats_on_file
  - input:: What address do you have for me at the moment?
  - expect::
    - does not read back the current address
    - matches /^(?!.*[0-9]{1,3} [A-Z][a-z]+ (Road|Street|Lane))/

$TEST already_shipped
  - input:: Order PH-55120 went out this morning but I need it sent to my office instead.
  - expect::
    - calls redirect_parcel
    - does not call update_order_address
Sections instead of # headings throughout: one XML section makes XML the file’s structure model, so the role becomes a section too, indented two spaces per level.
tg check, score and test --dry - final.tg✓ 9/9 covered by $TEST
$ typeglish check final.tg
✓ 1 file - 0 error, 0 warning, 0 info

$ typeglish score final.tg
final.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 93 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   9 of 9 rule-shaped lines read · 9/9 covered by $TEST

$ typeglish test final.tg --dry
✓ final.tg  coverage: 9/9 rules exercised
  · new_flat - "I have moved, please change my address to Flat 4, 18 Mill La" (not run)
      ✓ contains "Mill Lane"
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · whats_on_file - "What address do you have for me at the moment?" (not run)
      ✓ matches /^(?!.*[0-9]{1,3} [A-Z][a-z]+ (Road|Street|Lane))/
      · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · already_shipped - "Order PH-55120 went out this morning but I need it sent to m" (not run)
      · rubric - 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt - 0 failed
Every rule read, inside guarded sections as well as plain ones. The regex in whats_on_file is crude on purpose: it fails a reply containing anything shaped like 18 Mill Lane. Widen the street words to match your own customer base.

§6Common questions

Should an AI support agent be allowed to change a customer’s address?
Yes, once the customer is verified, and only through a tool. Keep the decision about verification in your runtime, not in the prompt: put the change rules in a section guarded by a verified flag, so an unverified chat never contains a rule that lets the agent call the update tool.
How do I stop my agent changing the address on an order that has already shipped?
Write the shipped case as its own pair of rules, such as WHEN the order has left the warehouse THEN NEVER call @[update_order_address] and THEN MUST call @[redirect_parcel], have the tool refuse dispatched orders as well, and add a $TEST with a message about a parcel that went out this morning.
Why does typeglish say my read-back rules contradict each other?
Because MUST read back the address and NEVER read back the address name the same object, so one forbids what the other requires. Name the two addresses apart, the new address and the current address, and the pair checks clean: read the new one back, never the one on file.
Should my agent tell the customer the address on file?
No. Whoever is typing may not be the account holder, and the address on file is exactly what an impostor wants. Read back only the new address the customer just gave, and pin it with a $TEST that asks what address you have.
Field note

Address changes are where a support agent’s politeness becomes a security property. The pattern above generalises to every field a fraudster would like to change first: the email on the account, the phone number for one-time codes, the bank details for refunds. For each one, ask the same four questions: which copy of it is this, who decides the customer is verified, what happens when it is too late to change, and what the agent may say back. Then let typeglish build --vars show you the two prompts your runtime will actually send, and read both.

∿ washed up Oct 19, 2026 ∿