← Tidelines/Guides

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.

by TypeGlish team8 min read#guides
Three items. One rule.

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.

basket.tg - the policy written as a loop✗ prompt/unparsed-rule
# 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.
tg review - basket.tg with the loop✗ 1 of 2 rules read
$ 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
Note the exit code: 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.

basket.tg - the basket as a typed input✗ 1 error, refused
$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.
tg check - basket.tg with a list input✗ structure/bad-input-type
$ 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
Blocking, and deliberately so. A $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:

basket.tg - the quantified rule✓ A (100/100)
# 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.
tg score - basket.tg, loop traded for quantifier✓ 2 of 2 rules read
$ 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
Same policy, same nine lines, and the rule is now visible: 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:

basket.tg - a per-item rule and a per-contact bound✗ A (100/100), nothing proven
# 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.
Three items missing, and these two lines disagree about whether that is three credits or one. 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:

basket.tg - two bounds on one unit✗ 4 error
# 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.
tg check - basket.tg, both bounds per item✗ logic/numeric
$ 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.
Read the slot name the checker printed: 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:

basket.tg - the finished file✓ A (100/100), 4/4 covered
$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
tg check, score, test --dry and build --bundle - basket.tg✓ 220728b0bc6f
$ 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)
One test case, four rules covered, because the case's expectations share the wording of the rules they exercise. That is also why the expectation reads approves a credit for every missing item and not credits all three: coverage is matched on the rule's own words.

And here is where the basket ended up. The prompt says the policy; the bundle carries the array:

.typeglish/dist/basket.txt and basket.agent.json✓ the list is a parameter
# 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."
        },
Four rules and one sentence of world in the artifact, with no item names in it at all. 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 item is 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 credit draws prompt/unparsed-rule at 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 with 0 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 strings is a blocking structure/bad-input-type error 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 $TOOL parameter, where items:: list of strings compiles into the bundle as type string with array 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 item beside MUST approve at most 1 credit is 0 error, 0 warning, 0 info at A (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 becomes 4 errors, logic/action-count and logic/numeric, the moment the two numbers actually disagree.
Field note

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.

∿ washed up Sep 20, 2026 ∿