Marketing

A Rules Table the Model Can Execute Without a Prompt Rewrite

Antonio Evans8 min read

A wide flat-lay of a printed spreadsheet on a desk, columns labelled Field, Condition, Action, Priority, with several rows highlighted in one column and a red pencil circling a single cell.
Key points

Key points

  1. 1Store each field-level inference rule as one row with field, condition, action and priority, and keep the rules in version control next to your code.
  2. 2A validator catches empty conditions, unknown fields and priority ties before any tokens are spent on a model call.
  3. 3A compiler sorts rows by priority and renders them into a compact block that is passed as data, so the system prompt stays constant across every run.
  4. 4Changing a business rule becomes a one-cell edit plus a rerun, and the diff in review shows exactly which rule moved.

Why prose rules rot in the system prompt

A system prompt is a single string that carries role, tone, tool guidance and every business rule you have ever written. The Tetrate guide on system prompts describes them as persistent context that defines role, behavior and boundaries, the layer that keeps responses consistent across a whole session the system prompts guide. That is exactly the wrong container for rules that change per customer, per region or per quarter.

Two failure modes show up quickly. First, edits become surgical in the worst way: to change one threshold you re-flow paragraphs, and a reviewer cannot tell whether you altered behavior or just wording. Second, long prompts lose their own instructions. Practitioners report that models "forget" format and constraint instructions placed thousands of tokens earlier, which is why one write-up recommends restating critical requirements immediately before generation the study on stable output format in long contexts. A rule buried on line 40 of a system prompt competes with everything after it.

There is also a governance argument. Thoughtworks' write-up on Structured Prompt-Driven Development treats prompts as first-class artifacts kept with the code in version control so changes stay reviewable and reusable the SPDD article on martinfowler.com. A rules table is that idea applied at field level: the artifact you review is structured, small and diffable.

Decision rule: if a rule names a field, compares a value, or selects an action, it belongs in the table. If it describes persona, tone or output schema, it belongs in the system prompt. If it does both, split it.

Designing the rules table schema

Four columns cover most field-level inference work, but you will want a few more to keep the table maintainable as it grows.

ColumnPurposeExample valueValidation
rule_idStable identifier for diffs and logsR-014Unique, non-empty
fieldTarget output field the rule writesrisk_tierMust exist in output schema
conditionBoolean expression over input fieldscountry == "DE" and revenue >= 50000000Parses, references known inputs
actionWhat to do when condition is trueset "elevated"One of a fixed action vocabulary
priorityInteger, lower runs first20Integer, unique within field
rationaleOne line on why the rule existsRegulatory threshold from complianceNon-empty
activeKill switch during migrationtrueBoolean

The action column is where discipline pays off. Keep it to a small verb set: set, append, escalate, require_review, suppress. Free-text actions force the model to interpret intent, which is the thing you are trying to remove.

Field descriptions matter more than they look. Reducto's guide to schema-based extraction notes that the model uses field names and descriptions to locate and produce values, so descriptions act as instructions the schema-based extraction guide. The same guide recommends keeping schemas bounded and avoiding asking for derived values, because requesting computed outputs can lead a model to fabricate them. Apply both rules here: extract and classify with the model, compute totals in code.

Here is a worked starting table for a lead-routing classifier:

rule_id,field,condition,action,priority,rationale,active
R-001,segment,employees < 50,"set ""smb""",10,Standard small-business cut,true
R-002,segment,employees >= 50 and employees < 500,"set ""midmarket""",20,Band between SMB and enterprise,true
R-003,segment,employees >= 500,"set ""enterprise""",30,Enterprise headcount band,true
R-004,priority_tier,country == "DE" and revenue >= 50000000,"set ""elevated""",10,Compliance threshold,true
R-005,priority_tier,intent_score >= 80,"set ""elevated""",20,High purchase intent,true
R-006,outreach_route,segment == "enterprise" and priority_tier == "elevated","escalate ""named_account""",10,Route top accounts to named reps,true
R-007,outreach_route,segment == "smb","suppress ""phone""",20,No phone coverage for SMB,true

Note that R-006 depends on fields written by earlier rules. That dependency is the reason priority exists as a column rather than as row order in a spreadsheet.

Writing the validator

The validator runs on every commit and rejects a table the model could misread. Four checks catch nearly all real breakage.

  1. Every field appears in the output schema, and every input named in a condition appears in the input schema.
  2. Every condition parses with your expression parser and evaluates without error against a fixture set of at least five inputs.
  3. priority values are unique within a field, and any rule that reads a field written by another rule has a higher priority than the writer.
  4. Every action verb is in the allowed vocabulary, and every quoted literal is non-empty.

A minimal implementation in Python, using a safe expression evaluator rather than eval:

import re

INPUT_FIELDS = {"country", "revenue", "employees", "intent_score"}
OUTPUT_FIELDS = {"segment", "priority_tier", "outreach_route"}
VERBS = {"set", "append", "escalate", "require_review", "suppress"}
IDENT = re.compile(r"\b[a-z_][a-z0-9_]*\b")

def validate(rows):
    problems = []
    seen = {}
    writers = {}
    for r in rows:
        if not r["active"]:
            continue
        if r["field"] not in OUTPUT_FIELDS:
            problems.append(f"{r['rule_id']}: unknown output field {r['field']}")
        names = set(IDENT.findall(r["condition"])) - {"and", "or", "not", "in", "true", "false"}
        unknown = names - INPUT_FIELDS
        if unknown:
            problems.append(f"{r['rule_id']}: unknown inputs {sorted(unknown)}")
        verb = r["action"].split(" ", 1)[0]
        if verb not in VERBS:
            problems.append(f"{r['rule_id']}: unknown action verb {verb}")
        key = (r["field"], int(r["priority"]))
        if key in seen:
            problems.append(f"{r['rule_id']}: duplicate priority {key}")
        seen[key] = r["rule_id"]
        reads = names & OUTPUT_FIELDS
        for dep in reads:
            writer_priority = writers.get(dep)
            if writer_priority is not None and writer_priority >= int(r["priority"]):
                problems.append(
                    f"{r['rule_id']}: reads {dep} before its writer runs")
        writers[r["field"]] = min(writers.get(r["field"], 10**9), int(r["priority"]))
    return problems

That last check is the one people skip and then debug for an afternoon. If a rule reads a field that the table itself produces, the writer must sort ahead of it, otherwise the model sees an instruction that references something it has not yet decided.

Compiling rows into a model-readable block

The compiler turns sorted rows into a compact section you inject as data. Three habits keep it reliable.

Inject, do not paraphrase. Pass the compiled block as a separate content section (a user message or a tool payload), not by string-interpolating it into the system prompt. The system prompt stays byte-identical across runs, which means prompt caches keep working and evaluation baselines stay comparable.

Sort and number. Emit rules in priority order with the rule_id visible. Models follow ordered lists better than unordered prose, and the IDs let you log which rule drove each output value later.

Restate the contract at the end. The recency effect is well documented in prompt practice: put a one-line reminder of the output shape after the rules, not before the long-context output format write-up. The checklist-style prompt study found checklist-improved prompts scored 7.50 out of 8 on a combined rubric, against 5.67 for raw prompts and 6.67 for clarifying-question prompts, while using fewer average tokens the structured prompting study. Structure did more work than extra words.

A compiled block looks like this:

RULES (evaluate in order; first matching rule per field wins unless action is append)
R-004 | field=priority_tier | if country == "DE" and revenue >= 50000000 -> set "elevated"
R-005 | field=priority_tier | if intent_score >= 80 -> set "elevated"
R-001 | field=segment | if employees < 50 -> set "smb"
R-006 | field=outreach_route | if segment == "enterprise" and priority_tier == "elevated" -> escalate "named_account"

OUTPUT: JSON object with keys segment, priority_tier, outreach_route. Use "unknown" when no rule matches.

Keep the block under roughly 50 rules per field group. Reducto's guidance to stay under about 50 fields in a schema is offered as a rule of thumb, not a hard limit the schema guide, and the same instinct applies to rule blocks: past a few dozen rows, split by task and make two calls.

A worked change: one cell, one rerun, one diff

Suppose compliance tells you the German revenue threshold moved from 50 million to 30 million euros. Under prose rules you would edit a paragraph in the system prompt, hope nobody's cached copy persists, and rerun the whole evaluation suite without a clear signal of what changed.

With the table, the edit is one cell:

-R-004,priority_tier,country == "DE" and revenue >= 50000000,"set ""elevated""",10,Compliance threshold,true
+R-004,priority_tier,country == "DE" and revenue >= 30000000,"set ""elevated""",10,Compliance threshold,true

Now the numbers. Take a fixture of 500 labeled records and assume the old threshold put 60 German accounts above the line and the new one puts 84. Your rerun should move exactly 24 accounts into elevated and leave 476 outputs unchanged. Write that as an assertion, not a hope:

changed = {r["id"] for r in new if old[r["id"]] != new[r["id"]]}
assert changed == expected_ids, f"unexpected movement: {changed ^ expected_ids}"

If the diff touches more than the 24 expected records, you have a second bug: a prompt interpolation, a stale cache, or a rule that accidentally reads the changed field. In practice this assertion catches more real incidents than any rubric score, because it tests the plumbing, not just the model.

Run the same pattern for every rule: one small fixture where the rule fires, one where it does not, and an expected set of changed record IDs. Store those IDs next to the rule in a test file, and you can validate a pull request in seconds.

If your rules span text, image and audio classification, one option is to keep the block identical across model types and send every call through a single endpoint; Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible API key, which keeps the compiler output in one shape Quanta Inference.

Migrating an existing prompt without breaking behavior

Do not rewrite the prompt and the rules in the same week. A four-step migration keeps behavior stable.

Step 1: Transcribe, do not improve. Convert each sentence in the current system prompt that names a field and a condition into a row. Where a sentence hides two rules ("enterprise or high-intent accounts get named reps"), split it into two rows. Set every active to true and keep the original prose in a comment column if you need it.

Step 2: Shadow-run. Send each record to both paths, the old prose prompt and the compiled table, and log disagreements. Any record where the two disagree is either a transcription bug or a genuine ambiguity in the old prose. Both are worth knowing.

Step 3: Flip on agreement. When shadow runs agree on your fixture set, point production at the compiled table. Keep the old prompt in version control for one release cycle.

Step 4: Delete the prose. Remove the rule sentences from the system prompt entirely. A prompt that still contains a stale copy of a rule is worse than either version alone, because the model now has two sources and no tie-breaker.

Migration stepArtifact under reviewPass condition
TranscribeRules CSV, one row per atomic ruleValidator returns zero problems
Shadow-runDisagreement logEvery disagreement explained or fixed
FlipProduction config changeOutput diff matches expected record set
DeleteSystem prompt diffNo rule sentences remain

Teams that want the transcription and shadow-run harness built for them can hand the prompt and a fixture file to Quanta Experts and get back the rules table, validator and expected-diff tests as finished deliverables Quanta Experts.

Where the table approach stops paying

A rules table is not free. Three costs are real.

Condition expressiveness. Once a rule needs nested logic across five fields with arithmetic, a flat condition string gets hard to read and harder to test. At that point, move the rule into code and let the table call a named function. The table stays the index; the code holds the math.

Model judgment beyond rules. Rules classify and route; they do not write the outreach message. Keep generation in a separate step that receives the classified fields as input. Mixing the two reintroduces the prose problem you just removed.

Schema drift. Output schemas evolve. The PARSE work from Amazon researchers argues that schemas are themselves a natural-language contract that models interpret, and that optimizing them for model consumption improved extraction accuracy by up to 64.7% on one benchmark while cutting extraction errors by 92% within the first retry the PARSE paper. Read that as a warning: your table's field names and descriptions are part of the contract, so rename and re-describe them as deliberately as you change conditions, and rerun the fixtures every time.

One more guardrail: log the rule_id that produced each output value. When a downstream team asks why an account was escalated, "R-004 fired" is an answer. "The model said elevated" is not.

FAQ

FAQ

How many rules can I put in one compiled block?

Keep a single block to a few dozen rows and split by task when it grows. Reducto's schema guidance treats around 50 fields as a practical rule of thumb, not a hard limit, and long blocks push later rules further from the model's attention the schema-based extraction guide. Two smaller calls with focused rule sets usually beat one large call.

Should the rules block go in the system prompt or a user message?

Put it in a user message or tool payload so the system prompt stays stable across runs. That keeps caching and evaluation baselines comparable, and it matches the pattern of restating critical requirements close to the point of generation described in output-format guidance the long-context output write-up. Changing a row then changes only the data, never the operating instructions.

How do I test a rule change without a full evaluation suite?

Keep three fixtures per rule: one that fires, one that does not, and one near-miss at the threshold boundary. Assert the exact set of record IDs whose output changes. The worked example above expects 24 changed records from one threshold move; anything else is a plumbing bug, and the assertion runs in seconds.

What belongs in the condition column and what should stay in code?

Anything a non-engineer might change belongs in the condition: thresholds, country lists, score cutoffs. Anything that needs arithmetic across many fields or an external lookup belongs in code, called by name from the action column. The schema guide's advice against asking a model for derived values applies here too the schema guide.

Does structuring rules as data count as prompt engineering or context engineering?

It sits at the boundary, and the distinction is useful. Prompt engineering tunes wording; context engineering decides what information the model sees and in what shape. A rules table is context engineering, and the writers who draw that line argue context is where reliability comes from the prompt versus context engineering comparison. That is a reasonable reason to invest in the table before investing in another prompt rewrite.

Sources

Sources

  1. System Prompts: Design Patterns and Best Practices
  2. Prompt Engineering: Stable output format in long contexts
  3. Structured-Prompt-Driven Development (SPDD)
  4. Schema-Based Document Extraction
  5. A Comparative Study of Structured Prompting
  6. PARSE: LLM Driven Schema Optimization for Reliable ...
  7. Prompt Engineering vs. Context Engineering: The True ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles