The hardest problem in document extraction isn’t reading the value off the page — modern models do that well. The hard problem is that the same value comes back in a dozen different shapes. A premium is $1,250,000 here and 1.25M there. A date is Jan 5 2025, or 01/05/25, or 2025-01-05. A yes/no answer is Y, true, “yes”, or a checkbox glyph. A missing field is “N/A”, an empty string, a dash, or simply absent. Every one of those variants is technically correct and operationally useless: the moment you try to sum a column, deduplicate two records, or score an extraction against a gold set, the inconsistency breaks you.

InsightXtract solves this with a single idea: an output contract that every agent inherits automatically. The contract defines exactly one canonical shape for each kind of value, and it is enforced on every field of every extraction — the Excess Casualty agent, the D&O agent, a freeform run on an unclassified PDF, a multi-document submission. There is no per-agent formatting code to maintain and nothing for a builder to re-declare. You say what a field is; the platform decides how it renders.

The governing principle: type declares intent, the platform decides format

Every field in an InsightXtract schema already carries a declared typedate, currency, number, percentage, boolean, text. That declaration is the only formatting input the platform needs. Formatting is type-driven and global: the field’s type keys directly into the contract, and the contract knows how that type must render. A builder never writes a formatting rule to get canonical money or canonical dates — declaring the field as currency or date is the whole instruction.

This is what keeps twenty-three agents from drifting apart. Because the format is derived from the schema that already exists rather than authored per agent, there is exactly one place the rules live, and every agent is bound by them the moment it declares a typed field.

The canonical rules

The contract is small, deliberate, and unambiguous. Each declared type maps to one canonical output and nothing else:

Declared typeCanonical outputExample (in → out)
date / datetimeISO YYYY-MM-DD stringJan 5 2025"2025-01-05"
currency / moneybare number — no symbol, no separators$100,000100000
number / integerbare number1,2001200
percentagedecimal ratio (a fraction)15%0.15
booleanexactly "Yes" or "No"Y / true"Yes"
any type, not foundJSON null"N/A" / "" / "-"null
email / phone / address / texttrimmed string Acme Co. "Acme Co."

Read those rules carefully, because the details are the point. Money is a bare number100000, not $100,000 and not the string "100000" — so it can be summed and compared without parsing. A missing value is JSON null, never the sentinel strings "N/A", "", or "-" that quietly poison aggregations. Placeholder tokens like N/A, none, a lone dash, or stray whitespace are all normalized to null. Fields are never dropped and never omitted — a not-found field is present with a null value, and an empty table comes back as rows: [], so the shape of the record is stable whether or not the data was there.

The one convention choice: percent basis

Percentages have two reasonable representations, so the contract makes an explicit choice: ratio. 15% becomes 0.15; 12.5% becomes 0.125, with up to four decimal places of precision preserved so a value like 12.50% is never truncated. Ratios are clean for downstream math and unambiguous — a bare 15 can never be mistaken for 15 percent. A field that legitimately arrives in points declares percent_basis: points and the contract divides by 100 for you. That is the single convention knob; everything else is fixed.

The key idea: two layers of enforcement

A prompt alone can’t guarantee format. Language models drift — ask for YYYY-MM-DD a thousand times and a handful of answers will still come back as 01/05/2025 or January 5th. So InsightXtract doesn’t rely on the prompt to be the guarantee. It uses two layers with clearly divided responsibilities:

  • Layer 1 — the extraction prompt (best-effort). A shared contract block is rendered into every extraction prompt — single-doc, freeform, spec-driven, and multi-document alike. It tells the model to emit near-canonical values: dates as YYYY-MM-DD, money and numbers bare, percentages as ratios, booleans as Yes/No, not-found as null. This gets the model most of the way there and keeps the raw output clean.
  • Layer 2 — the deterministic post-processor (source of truth). After extraction, a single chokepoint walks the schema, reads each field’s declared type, and coerces the value into the canonical shape. This layer is deterministic code, not a model. It runs regardless of what the model returned, so the guarantee holds even when the prompt is ignored.

The division of labor is the whole design. The prompt is a hint; the post-processor is the promise. Because the post-processor is type-driven, it needs zero per-agent configuration — it reads the same declared types the schema already has and applies the same contract to all of them.

flowchart LR A[Model emits
raw value
e.g. "$1,250,000"] --> B[Layer 1
Prompt hint
asks for near-canonical] B --> C[Layer 2
Deterministic contract
reads field type] C --> D{Type?} D -->|currency| E[bare number
1250000] D -->|date| F[YYYY-MM-DD
2025-01-15] D -->|percentage| G[ratio
0.15] D -->|boolean| H[Yes / No] D -->|not found| I[null] E --> J[Canonical
output record] F --> J G --> J H --> J I --> J

Raw model output passes through a prompt hint, then a deterministic, type-driven enforcement pass produces one canonical shape.

Before and after, in one field set

Here is what the two layers do to a realistic batch of messy raw output. The left is what an unconstrained model might emit for a handful of fields on an excess casualty submission; the right is what the contract guarantees.

Before — raw model output

{
  "policy_effective_date": "Jan 5 2025",
  "each_occurrence_limit": "$1,250,000",
  "total_power_units":    "1,200",
  "foreign_sales_pct":    "15%",
  "hazmat_hauled":        "Y",
  "experience_mod":       "N/A"
}

After — canonical, contract-enforced

{
  "policy_effective_date": "2025-01-05",   // date  → YYYY-MM-DD
  "each_occurrence_limit": 1250000,        // currency → bare number
  "total_power_units":    1200,           // number → bare number
  "foreign_sales_pct":    0.15,           // percentage → ratio
  "hazmat_hauled":        "Yes",          // boolean → Yes / No
  "experience_mod":       null            // not found → null, never "N/A"
}

Note that nothing here was configured on the agent. Each field carries a declared type; the contract did the rest. The same pass runs on scalar header fields and on every cell of every extracted table, so a loss-run row and a top-level premium obey identical rules.

Where the two layers meet the code

The enforcement isn’t bolted on at the edges — it is a single chokepoint that every extraction path funnels through, right after glossary standardization and just before the record is assembled for output. Whether the run is a classic single-document extraction, a multi-sheet workbook, an agentic single-doc pass, a freeform run on an unclassified file, or a unified multi-document submission, the same contract executes on the way out. Because there is one chokepoint and it is deterministic, the guarantee is uniform: the same input value produces the same canonical output on every agent, every time.

ConcernPrompt (Layer 1)Post-processor (Layer 2)
Date / money / number / percent / boolean / null formatHintEnforce — source of truth
Glossary standardizationOptional hintEnforce
Provenance / evidence spanSource of truthPass through
Per-agent formatting rulesNone neededNone needed — type-driven

A report of exactly what the contract changed is attached to each result’s metadata, so a reviewer can see that $1,250,000 was coerced to 1250000 rather than wondering whether the model simply returned it that way. The transformation is observable, not silent.

Why it matters

A canonical output shape sounds like a tidiness feature. It is actually the foundation everything downstream stands on:

  • Downstream systems just work. A policy admin system, a rating engine, or a data warehouse expects 1250000 as a number and 2025-01-05 as a date. When every agent emits exactly that, the integration is a straight load — no per-agent parsing, no defensive string-scrubbing, no surprise the day a new agent goes live.
  • Math is correct by construction. You can sum a premium column, average a loss ratio, or roll fleet counts up across a book the moment the data lands — because money and numbers are bare numbers and percentages are honest ratios, not strings dressed as values.
  • Deduplication is reliable. Matching two records of the same account depends on values being byte-for-byte comparable. $100,000 and 100000 and “100,000.00” are three different strings but one number; the contract collapses them to that one number so clearance and de-dupe don’t miss.
  • Gold-set scoring is meaningful. Evaluating an agent against ground truth requires the extraction and the gold value to be in the same shape. If the model returns “Jan 5 2025” and the gold set says 2025-01-05, a naive comparison scores a correct extraction as wrong. Canonicalizing both sides means the score measures accuracy, not formatting luck.
  • Nulls stop lying. Because not-found is always JSON null and never “N/A” or an empty string, “we didn’t find it” is unambiguous. Completeness metrics, required-field checks, and review queues can trust that a null means missing — not that a model chose a different placeholder that day.

Fewer knobs, one guarantee

Because formatting is derived from the field’s type rather than authored per agent, building a new agent doesn’t mean re-declaring how money or dates render. You define the schema and the type; the contract binds automatically. The configuration surface stays small and the guarantee lives in code — which is exactly why it holds across every agent instead of drifting one YAML file at a time.

One shape, everywhere

The difference between InsightXtract and a tool that returns raw model output isn’t the model — it’s the contract wrapped around it. Two layers, cleanly divided: a prompt that asks for near-canonical values, and a deterministic, type-driven post-processor that guarantees them no matter how the model drifts. Dates as YYYY-MM-DD, money and numbers bare, percentages as ratios, booleans as Yes/No, not-found as null — on every field, every table, every agent. The payoff isn’t neatness for its own sake. It’s that everything you build on top of the data — the integrations, the math, the dedup, the eval — can finally assume one shape and be right.