How to write agent rules for a multi-item order
The oat milk is missing, the eggs are missing, and the sourdough turned up crushed. Every rule in the prompt is written about one item, and the customer has three.
TL;DR A TypeGlish prompt has no list in it, on purpose: FOR EACH missing item THEN MUST approve a credit is prompt/unparsed-rule and the rules table marks it ✗ invisible, and $REQUIRE variable missing_items: list of strings is a blocking structure/bad-input-type reading a variable's domain is ONE value. Quantify instead (for every missing item), put the bound on the same unit (at most 1 credit per missing item), and hand the basket to a $TOOL parameter typed list of strings.
Greenfold Grocer delivers a weekly shop and handles the fallout in chat, with an agent called Nell. Substitutions, short-dated stock and crushed boxes are routine, and so is the shape of the contact: not one problem but three or four, arriving in a single message, each needing its own decision. The policy is per item. The prompt, written the week the agent launched, is a set of rules about the item, and the fastest-looking way to fix that is the way that does the least.
§1Do not write the loop
FOR EACH is a real control keyword. It appears in the operator table, it parses, and it looks exactly like the fix. Here is what it buys you.
# Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Nell, the delivery assistant for Greenfold Grocer. # Instructions @@ per_item: ops wrote the policy as a loop over the basket - FOR EACH missing item THEN MUST approve a credit. @@ brevity: a delivery chat stays short - MUST keep every reply to at most 3 sentences.
$ typeglish check basket.tg basket.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: FOR EACH bodies are not lowered yet. Fix: write the rule as one modal statement per item. ✓ 1 file - 0 error, 0 warning, 1 info $ typeglish review basket.tg basket.tg - A (94/100) → A (99) projected if every fix below lands check: 0 error, 0 warning, 1 info tiers: base+z3 FIXES ranked by projected gain +5 lead with a modal (MUST / NEVER / SHOULD) on L7 - 1 rule becomes visible to the checker RULES 1 of 2 rule-shaped lines read L7 ✗ invisible FOR EACH missing item THEN MUST approve a credit. FOR EACH bodies are not lowered yet → write the rule as one modal statement per item
0 error, so nothing stops this shipping. The rule inside the loop is gone as far as every proof is concerned, which means the credit policy is not compared against the refund cap, the brevity bound, or anything a later edit adds. The same prompt/unparsed-rule info fires on a THEN branch with no modal, which is where it turned up in rewrite: delete everything you have on me. It is the checker saying this looks like a rule and I could not read it, and it is worth treating as louder than info suggests.§2Do not declare the list either
Second instinct: if the basket is data, declare it as data. The host knows which items were missing, so make it an input and let the prompt point at it.
$REQUIRE variable missing_items: list of strings # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Nell, the delivery assistant for Greenfold Grocer. # Context @@ basket: the host is expected to fill the basket in The missing items are @{missing_items}. # Instructions @@ cover_the_basket: one rule for the whole basket - MUST approve a credit for every missing item.
$ typeglish check basket.tg basket.tg:1:1 error structure/bad-input-type Input "missing_items": A variable's domain is ONE value - a primitive or a "one of a, b" set, never a list ("list of strings"). ✗ 1 file - 1 error, 0 warning, 0 info
$REQUIRE variable exists to be branched on: $IF and $SWITCH ON resolve at compile time, and there is no branch over an array. One value per input is the same constraint that makes a router assume exactly one thing is wrong, which we went through in your router assumes one thing is wrong. It is not a missing feature so much as the boundary of the compile-time plane.So the prompt plane has no list in it at all: not as a loop, not as an input. That sounds like a limitation until you notice what a list in a prompt would be for. Every use of it is a rule applied to each element, and English already has words for that.
§3Write the quantifier instead
every is one of the quantifiers the checker reads, and it puts the whole basket inside a single rule the prover can work with:
# Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Nell, the delivery assistant for Greenfold Grocer. # Instructions @@ cover_the_basket: the quantifier does the work the loop could not - MUST approve a credit for every missing item. @@ brevity: a delivery chat stays short - MUST keep every reply to at most 3 sentences.
$ typeglish check basket.tg ✓ 1 file - 0 error, 0 warning, 0 info $ typeglish score basket.tg basket.tg - A (100/100) proven errors: none tiers: base+z3 planes runtime 100 (what the model reads) · hygiene 100 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 100 x.08 consistency 100 x.17 · structure 100 x.12 · annotation 100 x.12 style 100 x.08 · security 100 x.08 rules 2 of 2 rule-shaped lines read
1 of 2 becomes 2 of 2 and the info is gone. Worth saying plainly what changed and what did not. What the model does with a three-item basket is not guaranteed by this line. What changed is that the line is now in the proof, so every later edit gets compared against it.Prefer every to a number while you are at it. MUST approve a credit for each of the 3 missing items is a rule that goes stale the first time somebody loses four things, which is the trap taken apart in three payment options, two of them exist. A quantifier has no number in it to drift.
§4Put the bound on the same unit
This is the step that gets skipped, and it is where multi-item baskets actually go wrong in production. The credit rule is per item. The desk limit, written by somebody thinking about a single contact, is per conversation. Both lines are clean English and the prompt is silent about the collision:
# Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Nell, the delivery assistant for Greenfold Grocer. # Instructions @@ cover_the_basket: one rule for the whole basket - MUST approve a credit for every missing item. @@ desk_limit: the desk settles one credit per contact - MUST approve at most 1 credit.
typeglish check reports 0 error, 0 warning, 0 info and score reports A (100/100) with consistency 100. The bound counts credits per serve; the rule counts credits per item. They are not claims about the same quantity, so there is nothing for the prover to reconcile, and the model settles it in whichever direction the conversation leans.Say per missing item in the bound and both numbers land on one slot, which is exactly where the arithmetic tier can reach them. Here is the same pair the day the goodwill team adds a second credit for spoiled goods:
# Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Nell, the delivery assistant for Greenfold Grocer. # Instructions @@ per_item_cap: the desk settles at most one credit for each item - MUST approve at most 1 credit per missing item. @@ goodwill: a later edit from the goodwill team - MUST approve at least 2 credits per missing item.
$ typeglish check basket.tg basket.tg:7:1 error logic/action-count Conflicts with line 9. One action, two counts - "approve at most 1 credit" but "approve at least 2 credits". A directive carries ONE count for "approve credits per missing item" - state how many once. ↳ line 9: - MUST approve at least 2 credits per missing item. basket.tg:7:1 error logic/numeric Conflicts with line 9. Numeric conflict - "at most 1 credit" and "at least 2 credits" can't both hold. ↳ line 9: - MUST approve at least 2 credits per missing item. basket.tg:9:1 error logic/action-count Conflicts with line 7. One action, two counts - "approve at most 1 credit" but "approve at least 2 credits". A directive carries ONE count for "approve credits per missing item" - state how many once. ↳ line 7: - MUST approve at most 1 credit per missing item. basket.tg:9:1 error logic/numeric Conflicts with line 7. Numeric conflict - "at most 1 credit" and "at least 2 credits" can't both hold. ↳ line 7: - MUST approve at most 1 credit per missing item.
approve credits per missing item. The phrase per missing item is part of the address, which is why the earlier pair was silent and this one is four errors across two tiers. The unit of the bound is not decoration. It is what decides whether two numbers are about the same thing.A limit with no unit is a limit on nothing. Write the unit and the arithmetic starts working for you.
§5Hand the basket to the tool
The list still has to exist somewhere, because at some point three item names have to reach a system that can credit them. That somewhere is a $TOOL parameter, where list of strings is a legal type. One call carries the whole basket, rather than three calls the prompt has to count:
$TOOL approve_credits - description:: Credit the customer for the items that did not arrive. - input:: - items:: list of strings: the names of the missing items. - order_id:: string # Role @@ role: one sentence names the speaker, so every later "you" is the agent You are Nell, the delivery assistant for Greenfold Grocer. # Instructions @@ cover_the_basket: the quantifier carries the whole basket, so no item falls off the end - MUST approve a credit for every missing item. @@ per_item_cap: the bound sits on the same unit as the rule, so the pair is provable - MUST approve at most 1 credit per missing item. @@ one_call: the list is the tool's argument, so the basket crosses the boundary once - WHEN the customer reports a missing item THEN MUST call @[approve_credits]. @@ brevity: a delivery chat stays short - MUST keep every reply to at most 3 sentences. $TEST three_missing - input:: The oat milk, the eggs and the sourdough were all missing from today's delivery. - expect:: - approves a credit for every missing item - at most 3 sentences
$ typeglish check basket.tg ✓ 1 file - 0 error, 0 warning, 0 info $ typeglish score basket.tg basket.tg - A (100/100) proven errors: none tiers: base+z3 planes runtime 100 (what the model reads) · hygiene 100 (source only) facets enforceability 100 x.21 · hardness 100 x.12 · directness 98 x.08 consistency 100 x.17 · structure 100 x.12 · annotation 100 x.12 style 100 x.08 · security 100 x.08 rules 4 of 4 rule-shaped lines read · 4/4 covered by $TEST $ typeglish test basket.tg --dry ✓ basket.tg coverage: 4/4 rules exercised · three_missing - "The oat milk, the eggs and the sourdough were all missing fr" (not run) ✓ at most 3 sentences · rubric - 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY) ✓ 1 prompt - 0 failed $ typeglish build basket.tg --bundle ✓ built .typeglish/dist/basket.txt ← basket.tg (220728b0bc6f, full)
And here is where the basket ended up. The prompt says the policy; the bundle carries the array:
# Role You are Nell, the delivery assistant for Greenfold Grocer. # Instructions - MUST approve a credit for every missing item. - MUST approve at most 1 credit per missing item. - WHEN the customer reports a missing item THEN MUST call approve_credits. - MUST keep every reply to at most 3 sentences. // basket.agent.json, the items parameter { "name": "items", "required": true, "type": "string", "array": true, "description": "the names of the missing items." },
list of strings lowered to type: string with array: true in the sidecar, which is the schema your host hands the model. The basket is runtime data and it never entered the prompt.§6Common questions
- How do I make my AI agent handle several items in one order?
- Write one rule that quantifies over the items rather than a loop that walks them.
MUST approve a credit for every missing itemis a single rule the checker reads and proves, and it covers a basket of one or a basket of nine without mentioning a count. Then put the per-item bound in the same words,MUST approve at most 1 credit per missing item, and let the tool take the whole list as one typed array argument. The prompt states the policy once; the runtime holds the basket. - Can I use FOR EACH in a TypeGlish prompt?
- It parses, and it earns you nothing.
FOR EACH missing item THEN MUST approve a creditdrawsprompt/unparsed-ruleat info severity, reading FOR EACH bodies are not lowered yet, and the rules table marks the line invisible:1 of 2 rule-shaped lines read. The file still checks with0 error, so nothing stops you shipping it. What you lose is the enforceability credit and every proof that would have applied to the rule inside the loop. Write the quantified rule instead. - Can a $REQUIRE variable hold a list?
- No.
$REQUIRE variable missing_items: list of stringsis a blockingstructure/bad-input-typeerror reading a variable's domain is ONE value, a primitive or a one of a, b set, never a list. The list type is real, but it belongs to a$TOOLparameter, whereitems:: list of stringscompiles into the bundle astype stringwitharray true. That split is deliberate: prompt inputs are values the compiler can branch on, and there is no branch over an array. - Why does my agent credit only one of three missing items?
- Look for a bound whose unit is the conversation sitting next to a rule whose unit is the item.
MUST approve a credit for every missing itembesideMUST approve at most 1 creditis0 error, 0 warning, 0 infoatA (100/100), because the two lines are about different things as far as the prover is concerned, and the model resolves the clash however it likes. Add per missing item to the cap and the same pair becomes4 errors,logic/action-countandlogic/numeric, the moment the two numbers actually disagree.
The multi-item contact is underwritten in almost every CX prompt we read, and the reason is structural rather than careless. Prompts get drafted from a ticket taxonomy, and a taxonomy has one row per problem type, so the author writes one rule per row and the basket never appears. Then the transcripts arrive: the customer lists four things, the agent handles the first and the last, and the QA note says incomplete resolution. A useful exercise is to take your five most common issue types and ask which of your rules would still be correct if the customer raised three of them in one message. The rules that survive that reading tend to be the ones with a quantifier or a unit in them, and the ones that do not are usually the ones written about the item.