Founders

How to Constrain a Model to a Closed Source Table

Antonio Evans9 min read

A single rectangular table drawn as a grid of cells with labeled columns, enclosed by a solid border.
Key points

Key points

  1. 1Make one closed source table the only retrievable context by putting it behind a lookup function that accepts only a primary key and returns one row.
  2. 2Constrain identifier fields with a JSON Schema, ideally an enum built from the table's primary keys, so the model has fewer legal values to choose.
  3. 3Run a validator after generation that rejects any identifier not present in the table, and retry once with the invalid keys listed.
  4. 4Keep an audit log of every request, output, validation result and retry so you can prove what the model was allowed to see.

Why a closed table changes the whole problem

Most retrieval failures are not model failures. They are context failures. A retrieval step that can return documents, chunks or rows from anywhere gives the model a wide field of plausible facts to blend, and blending is exactly what a language model is good at. When the authoritative table is the only thing retrievable, the question changes from "is this answer true" to "is this identifier in the table", which is a question code can answer.

That distinction matters because schema-valid output is not the same as correct output. Constrained decoding and JSON Schema enforcement can guarantee that a response matches the schema, but they do not guarantee that the values inside it are real, and teams that stop measuring after adding a schema trade a visible failure mode for an invisible one, as a production write-up on structured output puts it. The same write-up notes that the enum violation case is syntactically valid, schema-invalid, and semantically reasonable from the model's point of view. A validator that checks membership against the table is what closes that gap.

The same lesson shows up in research on citation. A system that only retrieves documents improves the probability that output is correct, but a system that attributes claims creates verifiability, which is the stronger property (attribution and citation chapter). Your table's primary keys are that verifiability layer for structured data.

Worked example. Suppose a support team asks a model to write a summary of a customer's balance activity and to name the balance transaction IDs involved. There are 412 payments in the month, and the gap between 412 and the balance transaction count is expected, not a bug: a balance transaction is created for every movement of money into or out of the Stripe balance, not only for sales (Stripe balance transaction types). If the model can name any string that looks like a transaction ID, a reader has no way to check it. If the only legal values are the keys in a closed table, the reader can join on every one of them.

Design the closed table first

The table does not have to be the production source of truth. It has to be the only thing the model is allowed to read. Design it as a snapshot: one row per entity, one primary key column, plus the descriptive columns the model needs to write about the row, and explicit enum columns for any categorical field.

Design decisionDo thisWhy
KeysOne narrow primary key column, stable across snapshotsThe key is the only thing the model cites, so it must never be reused for a different row
DescriptorsOnly columns a reader could verify in the source reportUnverified prose invites the model to invent context
EnumsStore as fixed string columns with a closed value setThe enum becomes the schema's legal value list
HistoryDecide whether the row is current-state or as-of-dateMixing the two makes a quoted value wrong at one end
Snapshot timingLoad once per window, record the watermarkThe validator and reader must agree on what existed
Prohibited columnsRemove them from the snapshot entirelyAccess control plus schema plus validator is three layers

The history row is the one teams get wrong. If a dimension attribute changes and you keep several versions, you need a rule for which version the model may quote. Slowly changing dimension design treats this as an attribute-level decision: type 2 is the default where historical accuracy matters, and active-row identification by date range is the mechanism that handles late-arriving records correctly (SCD implementation strategies). For a closed table feeding a language model, the simplest safe rule is to snapshot the as-of-date value and put the snapshot date in the schema, rather than asking the model to pick a version.

If the table's categorical fields are themselves controlled vocabularies, generate the enum from the table rather than from the prompt. The Stripe documentation does this in practice: it separates transaction type from reporting_category and tells readers to use reporting_category for accounting classification, because type carries several names for the same commercial idea (balance transaction types). A model told to choose between "charge" and "payment" will sometimes choose wrongly and confidently, while a validator that checks a single canonical field will catch it. A practitioner walkthrough of the same API makes the point that the fee on a card charge lives as a field on the charge's own balance transaction rather than as a separate transaction, which is why fee handling has to be modeled explicitly and not inferred (Stripe API balance transactions).

Put the table behind a key-only lookup

The lookup function is the whole access control story. It accepts exactly one parameter, the primary key, and returns exactly one row. It does not accept search terms, filters, date ranges or natural language queries. If the model needs to find a key, a separate deterministic step does that and hands the model the key.

def lookup(key: str) -> dict | None:
    row = TABLE.get(key)          # TABLE is the closed snapshot, read-only
    if row is None:
        return None
    return row                     # one row, no joins, no aggregation

Three properties follow from this shape. First, there is no path from the lookup to any other table, so prompt injection inside a row's text field cannot widen the search space. Second, every row the model sees is one it named by key, so the audit log records intent as well as content. Third, a missing key returns None rather than a nearest match, which makes the failure explicit instead of silently plausible.

Verify this on the provider side too. Webhook and event-driven integrations in payments systems are the usual source of drift between what a system believes and what exists, and the documented best practices cover retries and out-of-order delivery, which are exactly the conditions that make a stale snapshot look like a fabricated key (Stripe webhooks). A production handler pattern goes further: insert the event ID under a uniqueness constraint before doing business work, so a replayed event cannot double-apply a state change (webhook idempotency pattern). Apply the same discipline to snapshot loads. Record the load ID, make the load idempotent, and stamp every row with it.

Give the model a schema whose identifier fields are enums

Two schema choices do most of the work. Every identifier field is an enum of the keys in the closed table, and every categorical field is an enum of the values actually present. Everything else is either a number copied from the row or free text that cannot be mistaken for an identifier.

{
  "type": "object",
  "additionalProperties": false,
  "required": ["summary", "citations", "reporting_category"],
  "properties": {
    "summary": { "type": "string", "maxLength": 1200 },
    "citations": {
      "type": "array",
      "minItems": 1,
      "items": { "enum": ["txn_1a2b", "txn_3c4d", "txn_5e6f"] }
    },
    "reporting_category": { "enum": ["charge", "refund", "payout", "fee"] },
    "snapshot": { "const": "2026-02-01" }
  }
}

Research on structured generation is clear about why this helps and where it stops. Constrained decoding masks invalid tokens at each step so the output conforms to the schema, and a benchmark of ten thousand real-world JSON schemas found that schema complexity is the largest controllable variable in reliability, with deeply nested objects and optional fields producing higher error rates than flat schemas (generating structured outputs from language models). The same source notes that constrained decoding was built specifically because probabilistic generation offers no structural guarantees. Keeping the schema flat and the identifier fields enumerated is therefore not a style preference, it is the main reliability lever you control.

A practical caution about enums built from live keys: a schema with five thousand enum values is large, slow to transmit and hard to keep in sync. Two options work. Cap the closed set by scoping the snapshot, for example one customer or one settlement window per request, so the enum stays in the hundreds. Or keep the identifier field as a plain string in the schema and rely entirely on the validator, accepting a higher rejection rate in exchange for a much smaller payload. The first option is usually better because it also reduces the model's opportunity to pick a wrong-but-valid key.

Validate every identifier after generation

The validator is short and it must be independent of the generation path. Do not rely on the schema engine, the prompt or the model's own claims about what it did.

import json

def validate(raw: str, keys: set[str], categories: set[str]) -> list[str]:
    errors = []
    try:
        out = json.loads(raw)
    except json.JSONDecodeError as e:
        return [f"parse_error: {e.msg}"]
    for k in out.get("citations", []):
        if k not in keys:
            errors.append(f"unknown_key: {k}")
    cat = out.get("reporting_category")
    if cat is not None and cat not in categories:
        errors.append(f"unknown_category: {cat}")
    return errors

Run it on the raw string, not on a parsed object that a library may have coerced. The failure taxonomy worth tracking explicitly includes preamble and postamble text, truncation at the token limit, field name drift, type coercion and enum violations, and each has a different cause and fix (structured output in production). A validator that returns typed errors rather than a boolean lets you see which of these is happening. Keep the parse failure rate as its own metric, separate from general error rate, so schema drift does not hide inside an aggregate.

One more check is worth the code: confirm that every number in the output appears in the referenced row. This catches the statement-level version of the problem, where the citation is real but the claim is not supported by it. Research on attribution makes the same distinction between a reference that does not exist and a reference that does not say what the output claims (attribution and citation).

Set a retry policy that fails closed

The retry policy is where most implementations quietly undo their own guarantees. Three rules cover it.

First, retry once. Send the model the original schema, the original rows and the list of rejected identifiers, and require it to choose from the returned keys only. A single corrective pass fixes most enum and key errors; a second pass mostly adds cost and latency.

Second, never repair in code. Do not swap a rejected identifier for the nearest string match, do not drop the field, and do not fall back to an earlier answer. A silent repair converts a detected failure into an undetected one, which is the exact outcome the closed table exists to prevent. Return a typed error to the caller and let the application decide.

Third, separate the retry from the request budget. Record how many attempts each request used and alert when the reject rate moves, because a rising reject rate usually means the snapshot and the live system have diverged, not that the model got worse. Detection research on fabricated references makes the same point in a different domain: automated verification only helps if it runs and its results are acted on (CheckIfExist).

A decision rule keeps this simple. If a rejected identifier appears in the snapshot but was excluded by scoping, the request is malformed and should be re-scoped, not retried. If it appears nowhere in the snapshot, retry once with the invalid keys listed. If the retry also fails, return the error and log it.

Log enough to answer the hard questions later

The audit log answers three questions: what the model was allowed to see, what it said, and what the system did about it. Log the request ID, the snapshot watermark, the exact key set passed to the schema, the model output verbatim, the validator's typed errors, the retry prompt, and the final disposition. Keep the log append-only and short enough to query.

This is the same reasoning that makes an events table the backbone of a reliable payments integration: write the record before the side effect, and let the constraint on the record be the deduplication signal (webhook idempotency pattern). Apply it to generation. Write the attempt row before the business action, and let a uniqueness constraint on request ID plus attempt number prevent a retried job from producing two contradictory outputs.

Two log fields earn their keep. The number of legal keys passed to the model, which tells you when scoping has drifted too wide. And the count of distinct keys the model actually cited, which is a crude proxy for whether it is spreading claims thinly across rows.

If you are assembling this pattern across several model providers and need the same request shape for text and structured output, Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible API key at https://quantaintelligence.ai/inference, which keeps the validation and logging layer identical regardless of which model answered.

FAQ

FAQ

Why not just retrieve the top matching rows and trust the prompt?

Because retrieval gives the model a probability of being grounded, not a guarantee. Attribution research frames retrieval as improving the chance of correctness while attribution creates verifiability, and only the second lets a reader check (attribution and citation). A key-only lookup plus a membership validator produces the verifiable version.

Does a JSON Schema with enums make the validator unnecessary?

No. Schema enforcement guarantees structure, and enum violations are one of several failure categories that stay syntactically valid while remaining wrong, as documented in production failure taxonomies (structured output in production). The validator checks against the table, which is the authoritative set, not against a copy embedded in a schema.

How large can the enum get before it hurts?

Treat schema complexity as the main controllable variable. Deep nesting and optional conditional fields raise error rates on both constrained and unconstrained generation (structured output benchmark), so scope the snapshot per request and keep both the nesting and the value count small. If the set cannot shrink, drop the enum from the schema and lean on the validator.

What should the retry send back to the model?

The original schema, the same rows, and an explicit list of the rejected identifiers with a statement that only returned keys are legal. One corrective pass is usually enough. Return a typed error instead of repairing the output in code, because a silent fix hides the failure the whole design exists to catch.

How do I handle a row whose attributes legitimately change?

Snapshot the value with an as-of-date in the schema and record the watermark in the log, rather than asking the model to choose between versions. Slowly changing dimension practice treats version selection as an attribute-level decision and warns that loose implementations quietly produce wrong answers (SCD implementation strategies).

Sources

Sources

  1. Structured Output for LLMs: Constrained Decoding - GMI Cloud
  2. Attribution and Citation: Sourcing LLM Outputs - Interactive
  3. Balance transaction types
  4. Slowly changing dimensions: implementation strategies
  5. Stripe API Balance Transactions: A Complete Guide
  6. Receive Stripe events in your webhook endpoint
  7. Stripe Webhook Idempotency, the Production Pattern
  8. Generating Structured Outputs from Language Models: Benchmark and ...
  9. Detecting Citation Hallucinations in the Era of AI-Generated Content
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