Prompts That Apply Business Rules
12 min read · updated August 4, 2026
A model applying business rules should not return a decision. It should return a decision and the rule that produced it — because the citation is what makes the output auditable, what makes a wrong answer diagnosable, and what tells you the rule table itself is broken.
The prompt
<rules>
id when then
R-01 order_total_eur >= 500 AND tier IN (silver, gold) free_shipping
R-02 order_total_eur >= 500 AND tier = bronze shipping_eur = 4.95
R-03 destination NOT IN (NL, BE, DE, FR) shipping_eur = 19.95
overrides R-01, R-02
R-04 any line has item_class = hazmat manual_review
R-05 account_age_days < 30 AND order_total_eur >= 1000 manual_review
R-06 tier = gold AND destination IN (NL, BE) free_shipping
</rules>
Apply <rules> to <case>. Use nothing but <rules>. If the rules do not decide
the case, that is the answer; do not fill the gap with what is usual.
Procedure, in this order:
1. List every rule whose "when" is satisfied by <case>, in id order.
2. Apply the overrides declared in the "then" column: a rule that overrides
another removes it from consideration.
3. If two surviving rules set the same field to different values, do not
choose between them. Return outcome "conflict" and name both.
4. If a rule's "when" refers to a field that <case> does not contain, do not
assume a default. Return outcome "insufficient_data" and name the field.
5. If no rule fires, return outcome "no_rule".
6. Otherwise return outcome "decided".
Return JSON:
{"outcome": "decided" | "conflict" | "insufficient_data" | "no_rule",
"decision": {"<field>": "<value>"},
"fired": [{"rule": "R-01", "because": "<the field values that satisfied it>"}],
"suppressed": [{"rule": "R-02", "by": "R-03"}],
"conflict": [{"field": "...", "rules": ["R-01", "R-02"]}],
"missing_fields": ["..."]}
"because" must quote the actual values from <case>, not restate the rule.
Write "order_total_eur = 640, tier = silver", not "the total is over 500".
<case>
{{case_json}}
</case>Why the citation is the product
A decision on its own is unfalsifiable. You cannot tell whether free_shipping came from R-01, from R-06, or from the model’s general sense that a large order from a good customer gets free shipping. All three produce the same answer on most cases and diverge on exactly the cases you care about.
With fired populated, three things become possible that were not before:
- Spot-checking without re-deriving. A reviewer reads rule id plus values and agrees or does not, in seconds. Without the citation they have to apply the whole table themselves, which is the work you were trying to avoid.
- Rule-level metrics. Count how often each rule fires. A rule that never fires is dead — either its condition is unreachable or it is shadowed by a rule earlier in the table, and both are bugs in the table that no amount of prompt tuning fixes.
- An answer to the customer. “Shipping was 19.95 because the destination is outside the standard zone (rule R-03)” is a sentence support can send. “The system decided” is not.
The because field must quote values rather than restate the condition, and the prompt says so with an example because models default to restating. “The total is over 500” is a paraphrase of the rule and proves nothing; “order_total_eur = 640” is a fact about the case that a reviewer can check against the case in one glance.
suppressed is the same idea applied to overrides. Knowing that R-02 would have fired and was removed by R-03 is what makes an override chain debuggable; without it, an override looks identical to a rule that never matched.
Conflict and insufficient data
These two outcomes are the difference between a rule applier you can trust and one you cannot, and both exist because the alternative is a silent guess.
Conflict
Real rule tables contain contradictions. They are written by different people at different times, and a pair of rules that both fire and set the same field differently is normal rather than exceptional. A model asked for a decision will resolve it — reasonably, invisibly, and differently on different days.
Returning conflict pushes the contradiction back to the people who own the rules. That is not a failure of the pipeline; it is the pipeline finding a bug that had been latent since the day both rules were written. Route conflicts to a human queue and count them: the count is a direct measure of the health of your rule table.
Insufficient data
The rule refers to account_age_days and the case does not have it. The model can guess a default, and the guess will usually be the permissive one, because permissive is what most examples look like. The explicit instruction — do not assume a default, name the field — turns a wrong decision into a data-plumbing ticket, which is what it actually is.
Both outcomes need somewhere to go in your code. An outcome field the caller ignores is worse than no field, because it creates the impression of rigour. The routing pattern in the triage recipe — model classifies, deterministic code routes — applies directly.
Writing the rule table
- One row, one rule, one id. The id is what everything downstream refers to. Never reuse one, never renumber, and treat the id as permanent even after the rule is deleted — your logs will contain it for years.
- Conditions in terms of fields, not prose.
order_total_eur >= 500is checkable against the case; “a large order” is not, and a model asked to decide what counts as large will decide differently for different cases. - Declare overrides explicitly on the row. Do not rely on order in the table, and do not rely on specificity. Both are conventions the model has to infer, and inference is the thing you are trying to remove.
- Keep the table under about forty rows in one prompt. Past that, the failure mode stops being “applies the wrong rule” and becomes “does not notice a rule”, which no output field can detect. Split by domain into separate calls instead.
- Test the table before you trust it. Generate a grid of synthetic cases across the boundaries in your conditions — just below and just above each threshold, each tier, each destination class — and run the whole grid. The conflicts and dead rules surface in an hour and would otherwise surface in production over a year.
Where this is the wrong tool
Stated plainly, because most pages on this subject will not: a model applying a rule table is a worse rule engine than a rule engine. It is slower, it costs money per evaluation, it is not deterministic, and it can fail in ways a decision table in code cannot. If your input is already structured and your rules are stable, write the rules in code and be done.
The pattern earns its place in exactly two situations.
- The input is unstructured. The hard part is not applying the rules, it is turning an email, a claim form or a call transcript into the fields the rules refer to. Do that with an extraction prompt — with evidence spans — and then apply the rules in code. This is the best architecture available for most of these problems, and it is the one this page is quietly arguing for.
- The rules change faster than you can deploy. A table edited by a policy team every few days, where a wrong decision is reversible and a review queue exists, is a reasonable place for the rules to live in a prompt.
Anything where a wrong decision has legal weight — credit, insurance, employment, benefits — needs a deterministic engine and a written justification, and in several jurisdictions the justification is a legal requirement rather than a nicety.
When it stops working
conflictandinsufficient_datago to zero. The strongest signal that the model has started deciding rather than reporting. Test it with a case you have constructed to be contradictory; if it returnsdecided, the procedure block is being outweighed and should move nearer the end of the prompt.becausedrifts back to paraphrase. Sample ten. Values, not restatements. Re-add the worked example if it has been trimmed.- A rule’s firing rate changes without the traffic changing. Either an adjacent rule was edited, or the model’s reading of a boundary condition shifted. Compare against the synthetic grid rather than against production.
- Cases arrive with the same fields missing repeatedly. Not a prompt problem at all. Fix the upstream extraction or the data plumbing; the prompt is doing its job by telling you.