Rewrite: your points expire on 31 December
Year-end is eight weeks away, and the loyalty team wants the bot ready for the January rush. The rewards FAQ goes into the prompt as written. It checks clean at a C, it tells the agent to be flexible about a rule the line above forbids it to bend, and it asks for a balance the agent has no way to know.
TL;DR A rewards FAQ pasted into a support prompt hides a ban and its exception in shapes the checker cannot compare; rewrite both with a verb it reads to surface the fight, let a points tool own the balance, the expiry date and the grace period, and test January’s customer before December ends.
Linden Beauty sells skincare and makeup online. Its chat assistant, Mira, handles orders and returns, and has never been asked about the rewards scheme because nobody put it in her prompt. Points expire on 31 December, and every January the queue fills with members whose balance went to zero overnight. This year the loyalty team wants Mira ready first. They have also decided something new: points that expired in the last 30 days can be restored, once per member per year. This is the rewrite that gets both into the prompt.
§1Before: the rewards FAQ
$TOOL start_return - description:: Start a return for an item in an order. - input:: - order_id:: string: the order the item came from. # Role You are Mira, the support assistant for Linden Beauty, an online skincare and makeup shop. # Constraints - MUST keep every reply to at most 4 sentences. - WHEN a customer asks for a refund THEN MUST call @[start_return]. # Linden Rewards Members earn 1 point for every pound they spend, and 100 points are worth 5 pounds off. Points expire on 31 December if they have not been used! - ALWAYS remind customers to use their points before they expire. - NEVER reinstate expired points. - Be flexible with loyal customers whose points have just expired. - MUST tell customers their points balance. - Points are usually added within 48 hours of delivery.
# Linden Rewards is the live prompt. The section is the top of the rewards FAQ, plus four rules the loyalty team wrote from it and a line from the help page about when points show up.$ typeglish check before.tg before.tg:14: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." before.tg:17:1 warn prompt/unmeasurable Unmeasurable rule — "be <quality>" names no observable action, so nothing can check compliance. Name a concrete action, or move it to a prose block (persona prose is allowed to be soft). before.tg:19: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, 1 warning, 2 info $ typeglish score before.tg before.tg — C (74/100) proven errors: none tiers: base+z3 planes runtime 82 (what the model reads) · hygiene 50 (source only) facets enforceability 70 x.21 · hardness 94 x.12 · directness 73 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 50 x.08 · security 100 x.08 lever annotation 0/100 (up to +12 overall) — 9 statements without a @@ note (L7 L10 L11 L14 L15 L16 L17 L18 +1 more) — put a "@@ why" line directly above each rules 5 of 7 rule-shaped lines read · 2 not provable · 1 unmeasurable L15 ~ unprovable ALWAYS remind customers to use their points before they expire. the checker knows "remind" but cannot prove this shape: it stops reading at "before" — the clause after it is not part of the provable action → if the clause is a condition, make it one: WHEN <clause> THEN MUST <verb> …; otherwise give it its own rule L16 ~ unprovable NEVER reinstate expired points. "reinstate" is not a verb the checker knows → use a verb from the catalog (redirect, escalate, ask, cite …) or add a numeric bound L17 ~ unmeasurable Be flexible with loyal customers whose points have just expired. no observable action or bound, so nothing can check compliance → name a concrete action or a bound ("at most 3 sentences") L19 = fact Points are usually added within 48 hours of delivery. a declared fact, not a rule — the world model reads it; it has no modal to enforce L17 prompt/unmeasurable −1 Unmeasurable rule — "be <quality>" names no observable action, so nothing can L14 prompt/agentless-passive −0.25 The doer is deleted (agentless passive) - who performs this? Name the actor or L19 prompt/agentless-passive −0.25 The doer is deleted (agentless passive) - who performs this? Name the actor or
§2What the clean run is telling you
No errors, a C, and a rules table that names four of the six lines under the new heading. Read it as a list of things the checker could not do, because each one is something the model will do instead.
Line 15 is a December spam rule. ALWAYS remind customers to use their points before they expire. is unprovable because the checker stops reading at before. What the model reads is always: in the last weeks of the year, the customer chasing a missing parcel and the customer with a rash from a serum both get a points reminder at the end of the reply. A reminder belongs in a conversation about points.
Line 16 and line 17 are one policy, split so nothing can compare them. NEVER reinstate expired points. uses a verb the checker does not know, so it is read but not proven. Be flexible with loyal customers whose points have just expired. is the warning, prompt/unmeasurable, because be flexible names no action. Together they are the loyalty team’s new grace period, written as a ban and a mood. The model has to work out on every chat where the ban ends and the flexibility starts, and a six-year member who asks nicely is exactly the case it will bend on. Your agent’s no is an opening bid is what that looks like a month later.
Line 18 reads clean, and it cannot be obeyed. MUST tell customers their points balance. is a well-formed rule with a catalog verb, and the rules table has nothing to say about it. Nothing in the prompt has a balance in it, though. Mira can only follow that rule by producing a number from somewhere, and the only places available are the customer’s own message and invention.
The rest is arithmetic and timing. 100 points are worth 5 pounds off asks the model to convert, and 31 December is a date it has to compare with today, which it does not know. The info on line 14 is about the passive at the end of the sentence; the sums and the date draw nothing. Both belong in the points system, which already knows the answer for each member.
Be flexible is not a softer rule. It is the exception to the ban, written in a shape nothing can compare with it.
§3Fix the shape, find the fight
Before deciding what the grace period should say, put the ban and the exception into a shape the checker can compare. Same verb, same object, the exception as a condition: NEVER offer a points restore. and IF the points expired in the last 30 days THEN MUST offer a points restore.
You are Mira, the support assistant for Linden Beauty. # Constraints - NEVER offer a points restore. - IF the points expired in the last 30 days THEN MUST offer a points restore.
$ typeglish check restore.tg restore.tg:4:1 error logic/contradiction Conflicts with line 5. Logical conflict — "offer a points restore" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 5: - IF the points expired in the last 30 days THEN MUST offer a points restore. restore.tg:5:1 error logic/contradiction Conflicts with line 4. Logical conflict — "offer a points restore" is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...). ↳ line 4: - NEVER offer a points restore. ✗ 1 file — 2 error, 0 warning, 0 info
That is the fight the FAQ was hiding. Never reinstate and be flexible were always a contradiction. The C version shipped it, and the model settled it differently in every chat. Once both lines are readable, the checker will not build until somebody decides.
The decision is the loyalty team’s, and they have made it: 30 days, once per member per year. That is two facts about a member’s history that Mira cannot see, so it goes where the history lives. A check_points tool returns the balance, its value in pounds, the next expiry date, and whether a restore is available. A restore_points tool performs the restore and refuses a second one in the same year. The prompt is left with a pair of rules scoped to what the lookup found: IF the points check finds a restore THEN MUST offer a points restore. and IF the points check finds no restore THEN NEVER offer a points restore. The old ban survives, scoped to the case the points system has ruled on.
The balance gets the same treatment: WHEN the customer asks about points THEN MUST call @[check_points]., plus a ban that holds until it has run, IF the points check has not run THEN NEVER state a points balance. The reminder moves onto the same event, so it only appears when the customer has raised points. The date leaves the prompt. The tool returns each member’s own expiry date, so the model never has to count the days to 31 December itself. Bake-off: 15 January vs. 2027-01-15 shows what the checker makes of a day-first date if you do keep one.
§4After: a points tool, scoped rules, three tests
$TOOL start_return - description:: Start a return for an item in an order. - input:: - order_id:: string: the order the item came from. $TOOL check_points - description:: Look up a member's points balance, its value in pounds, the next expiry date, and whether expired points can be restored. Points that expired in the last 30 days can be restored once per member per year. - input:: - member_email:: string: the email on the member's account. $TOOL restore_points - description:: Restore points that expired in the last 30 days. It refuses a member who has already had a restore this year. - input:: - member_email:: string: the email on the member's account. # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Mira, the support assistant for Linden Beauty, an online skincare and makeup shop. # Context @@ earn_rate: a fact the agent can explain; the points system does the sums A Linden Rewards member earns 1 point for every pound spent. @@ credit_time: answers "where are my points?" without a lookup for a parcel still in transit Points arrive within 48 hours of delivery. # Constraints @@ brevity: a chat bubble, not an email - MUST keep every reply to at most 4 sentences. @@ refunds: unchanged from the live prompt - WHEN a customer asks for a refund THEN MUST call @[start_return]. @@ lookup: the balance, the value and the expiry date all come from the points system - WHEN the customer asks about points THEN MUST call @[check_points]. @@ no_guessing: a balance only exists once the lookup has returned it - IF the points check has not run THEN NEVER state a points balance. @@ expiry_reminder: the reminder belongs to a points conversation, not to every reply in December - WHEN the customer asks about points THEN MUST remind the customer of their expiry date. @@ restore_yes: the points system decides who gets the grace period, not the agent's sympathy - IF the points check finds a restore THEN MUST offer a points restore. @@ restore_no: the old ban, scoped to the case the points system has ruled on - IF the points check finds no restore THEN NEVER offer a points restore. @@ restore_call: an accepted offer goes through the tool, which enforces once a year - WHEN the customer accepts a points restore THEN MUST call @[restore_points]. $TEST balance - input:: How many points do I have, and when do they run out? - expect:: - at most 4 sentences - calls check_points before it states a points balance - reminds the customer of their expiry date $TEST just_expired - input:: My points disappeared on New Year's Day. I had 600 of them. Can you put them back? - expect:: - calls check_points - offers a points restore when the points check finds one - calls restore_points once the customer accepts $TEST expired_in_march - input:: I lost 400 points last March. I have been a member for six years, surely you can give them back? - expect:: - matches /^(?![\s\S]*\brestor)/i - calls check_points - does not offer a points restore
The three tests are the three members the FAQ was written for. balance asks the question that line 18 could only answer by guessing. just_expired is the January chat, 600 points gone on New Year’s Day, and it expects the offer and the restore call. expired_in_march is the six-year member asking for a favour the policy does not cover. Its deterministic assert, matches /^(?![\s\S]*\brestor)/i, fails if restore, restored or restoring appears anywhere in the reply, with no judge involved. That is the line that catches flexible creeping back in.
$ typeglish check mira.tg ✓ 1 file — 0 error, 0 warning, 0 info $ typeglish score mira.tg mira.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 89 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 8 of 8 rule-shaped lines read · 8/8 covered by $TEST $ typeglish test mira.tg --dry ✓ mira.tg coverage: 8/8 rules exercised · balance — "How many points do I have, and when do they run out?" (not run) ✓ at most 4 sentences · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) · just_expired — "My points disappeared on New Year's Day. I had 600 of them. " (not run) · rubric — 3 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) · expired_in_march — "I lost 400 points last March. I have been a member for six y" (not run) ✓ matches /^(?![\s\S]*\brestor)/i · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY) ✓ 1 prompt — 0 failed $ typeglish build mira.tg ✓ built .typeglish/dist/mira.txt ← mira.tg (b4c19f4aa82b, full)
matches asserts run on every reply. The tool expectations are judged on a live run.§5Common questions
- How do I add a loyalty points program to my AI support agent's prompt?
- Give the agent a points lookup tool that returns the balance, its value, the expiry date and any restore the member qualifies for, and a rule that calls it when the customer asks about points. Keep the earn rate as a fact in context, keep the expiry date and the sums in the tool, and test the balance question, the just-expired member and the member outside the policy.
- Why does my chatbot make up a points balance?
- Because a rule tells it to state the balance and nothing gives it one. Add a lookup tool, a rule that calls it when the customer asks about points, and a ban that holds until the lookup runs: IF the points check has not run THEN NEVER state a points balance.
- How do I let my support agent make an exception to a rule?
- Write the exception as a decision the agent can check, not a mood. Be flexible is unmeasurable and quietly overrides the rule beside it. Put the exception's criteria in a tool, and scope the rule and the exception to what the tool found, so each case has exactly one instruction.
- Can TypeGlish tell when a rule and its exception conflict?
- Yes, once both use a verb it knows on the same object. An unconditional NEVER beside a conditional MUST on the same action is a logic/contradiction error and the build is refused. Written with an unknown verb, or as be flexible, the same pair checks clean while the conflict is still there for the model.
Loyalty schemes change at the turn of the year: a new earn rate, a new tier, a new expiry rule. After this rewrite, each of those lands in the points system, and Mira’s prompt only changes when what she does changes. When it does change, ship it as its own build. typeglish build mira.tg wrote this one as b4c19f4aa82b, and in February that hash is how you answer what Mira was telling members about expired points in January. If the FAQ you are pasting has its own use your best judgment line, Rewrite: the sentence that repeals the rest of your prompt takes that one apart.