How to write complaint-handling rules for an AI support agent
A customer tells the agent they are fed up with their bill. The agent apologizes, fixes the bill, and closes the chat. Nobody logs a complaint, because the prompt told the agent to fix things first and log them only if that failed.
TL;DR Complaint rules in an agent prompt fail in four places: a hedged "resolve it first" line the checker cannot read, a trigger that waits for the word complaint, a reference the model has to invent, and a compensation ban that contradicts its own paid exception; fix them with a dissatisfaction trigger, a logging tool called first, an UNLESS on the ban, and a $TEST per rule.
Fernhill Energy runs a chat assistant called Wren. Its complaints section grew the usual way: the operations lead wanted fewer complaints logged, compliance wanted every complaint logged, the complaints team wanted its two deadlines quoted, and finance wanted no money promised. Four owners, seven lines. Here is how to rebuild that section so the checker can hold every line to account.
§1The block most prompts start with
# Role You are Wren, the support assistant for Fernhill Energy. # Constraints - Try to resolve the issue before logging a complaint. - WHEN the customer wants to make a complaint THEN MUST log the complaint. - NEVER talk the customer out of a complaint. - MUST tell the customer we reply within 5 working days. - MUST tell the customer the complaint is resolved within 8 weeks. - NEVER promise compensation. - MUST give the complaint reference.
$ typeglish check complaints-draft.tg complaints-draft.tg:5:3 info prompt/hedging Hedging — "Try to" turns this instruction into a suggestion the model may skip. Delete the hedge, or commit to a modal (MUST / NEVER / SHOULD). complaints-draft.tg:5: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: no modal (MUST / NEVER / SHOULD) and "Try" is not a verb the checker knows, so it reads as prose. Fix: lead with a modal, or start with a verb from the catalog (Escalate …, Ask …). complaints-draft.tg:5:1 info prompt/unintroduced-definite "the issue" retrieves something this document never introduces - a model must guess which issue is meant. Mention an issue on an earlier line, or anchor it here ("their issue", "the issue you ..."). complaints-draft.tg:5: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. complaints-draft.tg:9: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." complaints-draft.tg:11:1 info prompt/unintroduced-definite "the complaint reference" retrieves something this document never introduces - a model must guess which reference is meant. Mention a reference on an earlier line, or anchor it here ("their reference", "the reference you ..."). ✓ 1 file — 0 error, 0 warning, 6 info $ typeglish score complaints-draft.tg complaints-draft.tg — C (76/100) proven errors: none tiers: base+z3 planes runtime 85 (what the model reads) · hygiene 50 (source only) facets enforceability 83 x.21 · hardness 87 x.12 · directness 73 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 53 x.08 · security 100 x.08 lever annotation 0/100 (up to +12 overall) — 8 statements without a @@ note (L2 L5 L6 L7 L8 L9 L10 L11) — put a "@@ why" line directly above each rules 6 of 7 rule-shaped lines read · 2 not provable L5 ✗ invisible Try to resolve the issue before logging a complaint. no modal (MUST / NEVER / SHOULD) and "Try" 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 …) L7 ~ unprovable NEVER talk the customer out of a complaint. "talk" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound L10 ~ unprovable NEVER promise compensation. "promise" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound L5 prompt/hedging −0.25 Hedging — "Try to" turns this instruction into a suggestion the model may skip L5 prompt/unintroduced-definite −0.25 "the issue" retrieves something this document never introduces - a model must L5 prompt/unregistered-doer −0.25 A bare generic doer never enters the world model - instruction to the agent, o L9 prompt/agentless-passive −0.25 The doer is deleted (agentless passive) - who performs this? Name the actor or L11 prompt/unintroduced-definite −0.25 "the complaint reference" retrieves something this document never introduces -
The line that matters most is the one the checker cannot see at all. Try to resolve the issue before logging a complaint has no modal and opens on a verb the checker does not know, so it ships to the model as prose: no enforceability credit, no conflict check against the rule below it. And it is the line that decides the outcome of most conversations. It tells the model that a logged complaint is what happens when the agent has failed, so a model that wants to succeed will fix the bill and stay quiet.
Two more lines are read but not proven. Talk the customer out of and promise are not verbs in the checker's catalog, so the rules exist but nothing can compare them with anything else. The intent of both is right. The wording gives the checker nothing to hold.
§2Decide what counts as a complaint
The draft waits for the customer to want to make a complaint. Customers rarely say that. They say the bill is wrong again, that they have been on hold for an hour, that they are fed up. The usual definition, the one the ISO 10002 complaints-handling standard uses, is any expression of dissatisfaction, whether or not the word complaint appears.
That makes the trigger a policy decision, not a writing one, and it belongs in the prompt as a condition the model can apply. Every WHEN is a classifier the model runs on each message; Every WHEN is a classifier covers how to write one that fires on the right messages. For Wren, Fernhill chose the middle path: the agent notices dissatisfaction and offers to log a complaint, and the customer decides.
If your trigger waits for the word complaint, your complaint log measures vocabulary, not dissatisfaction.
If your sector requires every expression of dissatisfaction to be recorded, skip the question: make the THEN the tool call itself. Either way, write it down. A trigger nobody chose is still a choice, and the model makes it.
§3Log first, and let the tool own the reference
The fix for the gatekeeping line is an order of operations. When the customer asks to complain, the agent calls the logging tool before it does anything else. Once the case exists, fixing the bill afterwards is good service, not a way to make the complaint disappear. That is also why never talk the customer out of a complaint does not need to survive: the order does its job, with a verb the checker can read.
The reference number comes from the same call. The draft said give the complaint reference, and the checker flagged it as a definite with nothing behind it: no line ever says where a reference comes from, so the model has to produce one. Why your agent invents a case reference walks through what that looks like in a transcript. Declare a $TOOL that returns the reference, call it with @[log_complaint], and make citing the returned reference its own rule.
§4Write the paid case into the ban
Finance wrote never promise compensation. Fernhill also pays a fixed amount when an engineer misses a booked appointment, and the operations lead added that as a rule further down. Swap promise for offer, a verb the checker reads, and the two lines meet:
# Role You are Wren, the support assistant for Fernhill Energy. # Constraints - NEVER offer compensation. - WHEN the customer reports a missed engineer appointment THEN MUST offer compensation. - MUST tell the customer that the complaints team replies within 5 working days. - MUST tell the customer that every complaint gets a final answer within 8 weeks.
$ typeglish check compensation.tg compensation.tg:5:1 error logic/contradiction Conflicts with line 6. Logical conflict — "offer compensation" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 6: - WHEN the customer reports a missed engineer appointment THEN MUST offer compensation. compensation.tg:6:1 error logic/contradiction Conflicts with line 5. Logical conflict — "offer compensation" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 5: - NEVER offer compensation. ✗ 1 file — 2 error, 0 warning, 0 info
This is the useful kind of error: the unprovable draft hid it, and the readable verb exposed it. The fix is to write the exception into the ban, UNLESS the customer reports a missed engineer appointment THEN NEVER offer compensation, so the two rules cover different conversations by construction. That shape, a default and its named exceptions, is the subject of Your prompt is a default and its exceptions. Anything else, including goodwill gestures, goes to the complaints team, which is where a person can weigh it.
§5Quote the team's clocks, do not own them
The complaints team has two deadlines: it acknowledges a complaint within 5 days and sends a final response within 8 weeks. Written as two rules on the agent, they collide:
# Role You are Wren, the support assistant for Fernhill Energy. # Constraints - MUST answer every complaint within 5 days. - MUST answer every complaint within 8 weeks.
$ typeglish check clocks.tg clocks.tg:5:1 error logic/time-strength Conflicts with line 6. One bound per slot — "within 5 days" already entails "within 8 weeks"; the looser rule on "you answer every · complaint" is dead weight. Keep one. ↳ line 6: - MUST answer every complaint within 8 weeks. clocks.tg:6:1 error logic/time-strength Conflicts with line 5. One bound per slot — "within 5 days" already entails "within 8 weeks"; the looser rule on "you answer every · complaint" is dead weight. Keep one. ↳ line 5: - MUST answer every complaint within 5 days. ✗ 1 file — 2 error, 0 warning, 0 info
The two numbers are two different events, and neither one is something the agent does. A chat assistant cannot answer a complaint within 8 weeks; it is gone when the chat ends. Move both into # Context as facts about the complaints team, each with its own verb, and give the agent the one job it actually has: tell the customer both timelines once the complaint is logged. Here is the whole section, rebuilt:
$TOOL log_complaint - description:: Open a complaint case and return its reference. - input:: - summary:: string: the complaint in the customer's own words. # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Wren, the support assistant for Fernhill Energy. # Context @@ ack_clock: the team's first clock, owned by people, quoted by the agent The complaints team acknowledges every complaint within 5 days. @@ final_clock: the team's second clock, a different event from the first The complaints team sends a final response within 8 weeks. # Constraints @@ trigger: dissatisfaction is the trigger, not the word complaint - WHEN the customer expresses dissatisfaction with Fernhill THEN MUST ask whether they want to log a complaint. @@ log_first: logging comes before any attempt to fix the problem - WHEN the customer asks to complain THEN MUST call @[log_complaint]. @@ reference: the reference comes from the tool, never from the model - WHEN @[log_complaint] returns a reference THEN MUST cite the returned reference. @@ clocks: the agent quotes the team's clocks, it does not own them - WHEN a complaint is logged THEN MUST explain both complaint timelines. @@ compensation: one paid case is written as the exception to the ban - UNLESS the customer reports a missed engineer appointment THEN NEVER offer compensation. @@ missed_visit: the fixed payment for a missed visit is policy, not goodwill - WHEN the customer reports a missed engineer appointment THEN MUST offer compensation. $TEST unhappy - input:: This is the third bill this year that has been wrong. I am fed up. - expect:: - asks whether they want to log a complaint - does not offer compensation $TEST complain - input:: I want to make a formal complaint about my bill. - expect:: - calls log_complaint - cites the reference from the tool - explains both complaint timelines $TEST missed_visit - input:: Your engineer never turned up on Tuesday and I took the day off work. - expect:: - offers compensation for the missed engineer appointment
$ typeglish check complaints.tg ✓ 1 file — 0 error, 0 warning, 0 info $ typeglish score complaints.tg complaints.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 87 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08 lever directness 87/100 (up to +1 overall) — Use the imperative or a named doer ("Verify the order.", "You must ..."), keep the active voice (not "is verified" / "a review must take place"), and make every it/this point at something the prompt introduced. rules 6 of 6 rule-shaped lines read · 6/6 covered by $TEST $ typeglish test complaints.tg --dry ✓ complaints.tg coverage: 6/6 rules exercised · unhappy — "This is the third bill this year that has been wrong. I am f" (not run) · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) · complain — "I want to make a formal complaint about my bill." (not run) · rubric — 3 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) · missed_visit — "Your engineer never turned up on Tuesday and I took the day " (not run) · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) ✓ 1 prompt — 0 failed
The three tests are the three conversations that matter: a customer who is unhappy but never says complaint, one who asks for a formal complaint, and the one paid exception. --dry validates them offline and confirms every rule is exercised; the prose expectations are judged on a live run.
§6Common questions
- How do I stop my AI support agent from deflecting complaints?
- Remove any line that tells the agent to resolve the problem before logging a complaint, and replace it with an order: when the customer asks to complain, the agent calls the complaint-logging tool first. Once the case exists, fixing the problem is good service rather than a way to avoid the log. In TypeGlish that is a rule like WHEN the customer asks to complain THEN MUST call @[log_complaint], which the checker can read and prove.
- What counts as a complaint for an AI customer service agent?
- The common definition, used by the ISO 10002 complaints-handling standard, is any expression of dissatisfaction, whether or not the customer uses the word complaint. Decide whether your agent should offer to log one or log it automatically, and write that decision into the prompt as a WHEN condition, because a trigger that waits for the word complaint will miss most of them.
- Should my AI agent offer compensation when a customer complains?
- Only in the cases your policy fixes in advance, written as named exceptions to a ban. A global NEVER offer compensation next to a guarded MUST offer compensation is a contradiction the checker refuses to build; write the ban as UNLESS the exception THEN NEVER offer compensation, and send every other request to the complaints team.
- How do I test complaint handling in an agent prompt?
- Write one $TEST case per conversation that matters: a customer who is unhappy but never says complaint, a customer who asks for a formal complaint, and each paid exception. Make each expectation share the wording of the rule it checks, so typeglish test --dry can confirm offline that every rule is covered before any live run.
Count the owners before you count the lines. A complaints section usually has three or four, and each wrote their line without reading the others. Put every complaint rule in one file, run typeglish score, and read the rules table first: an invisible or unprovable line is a rule nobody can check against its neighbors, and in a section with four owners, that is where the contradictions wait.