- Routine documents posted to the core system without manual keying
- Every extracted field validated against a schema and business rules before posting
- Exceptions handled in one queue with the document and the reason side by side
- An audit trail from each posted value back to its source document and reviewer
Anonymised composite. Client names, identifying details and confidential figures are withheld; public facts are cited in the references.
The situation
A property operator onboarded tenants and paid suppliers through a core property management system. Everything that fed it arrived as documents: signed leases, identity documents, proof of address, supplier invoices and service contracts. Each was opened, read and keyed by hand. Onboarding waited on the queue, keying errors surfaced later as billing disputes, and the team spent its time transcribing rather than checking.
Earlier attempts with template-based OCR had struggled because the documents varied too much: different lease templates, scanned and photographed IDs, and invoices in every layout a supplier could invent.
What had to be true
- Every value validated before it reaches the core system, by rules the business can read.
- The model never writes: it proposes a record, and deterministic code decides what is posted.
- Uncertain documents go to people, with the reason and the source region visible.
- Personal data handled deliberately: limited access, defined retention and no identity documents in logs or prompts beyond what extraction needs.
- Traceability from any posted value back to the page it came from.
Architecture
Each document type had a versioned JSON Schema describing its fields, types, formats and required properties1. The model was asked to return output conforming to that schema, and the same schema validated the response, so a malformed answer failed loudly instead of slipping through. More on the pattern in structured outputs for reliable LLM integration.
The data flow, end to end
- ReceiveStore the original unchanged; record channel, uploader and checksum
- ClassifyIdentify document type and split multi-document uploads
- ExtractVision model returns a schema-shaped record with a source region for each field
- ValidateSchema check, business rules, and cross-checks against tenant, unit and supplier data
- RouteAuto-post when every field passes thresholds; otherwise queue with reason codes
- PostWorkflow engine writes to the core system once, with an idempotency key, and logs lineage
The audit and staging tables in Postgres carried their own check, not-null and foreign key constraints, so invalid states were rejected by the database as well as by the application3. Retries were safe because posting was idempotent, the same discipline described in idempotent payment and order pipelines.
The hard part: confidence you can act on
Extraction quality was rarely the blocker. Deciding when to trust it was. A model's own statement of certainty is not a reliable signal, so confidence was built from evidence instead:
- Arithmetic and consistency checks: invoice lines must sum to totals, lease dates must be in order, rent schedules must match the term.
- Reference checks: the unit, tenant or supplier must exist and match what the document claims.
- Field-level thresholds set per document type from a labelled sample, with stricter rules on fields that move money or identify people.
- Reason codes on every exception, so the team could see which rule or field caused most reviews and fix the cause.
Documents are also untrusted input. Text inside an uploaded file can carry instructions aimed at the model, a form of indirect prompt injection2. The defence was architectural: the extraction model had no tools and no credentials, its output was confined to the schema, and posting was done by code that ignored anything outside defined fields. See prompt injection defence.
The model is the least trusted component in the pipeline. Design so that the worst it can do is produce a record that fails validation.
How the engagement ran
- Classify and extract
- Validate and score
- Auto-post or queue
- Open queued document
- Correct highlighted fields
- Approve for posting
- Review reason-code trends
- Adjust rules and thresholds
- Approve schema changes
- Document inventory
- Field and rule catalogue
- Data protection review
- Schema and extraction
- Labelled sample
- Review UI
- Cross-checks and thresholds
- Idempotent posting
- Monitoring
- Further document types
- Runbooks
- Change process for schemas
Ongoing monitoring of exception rates, corrections and drift by document type followed the measure-and-manage approach of the NIST AI Risk Management Framework4.
Key decisions and trade-offs
| Decision | Alternative | Why |
|---|---|---|
| Vision LLM with schemas | Template OCR per layout | Handles layout variety without a template for every supplier |
| Deterministic posting | Agent with system access | Model holds no credentials; injection cannot cause writes |
| Evidence-based confidence | Model self-reported scores | Thresholds grounded in checks and measured error rates |
| Reason-coded exceptions | Generic review queue | Shows which rule or field to improve next |
| Versioned schemas per type | One generic document schema | Clear validation and safe, reviewable change |
What I'd do again, and differently
Again: keep the model away from credentials and let validation, not the model, decide what is posted. Differently: build the labelled sample before the pipeline. Every threshold discussion went faster once there was a shared set of real documents to measure against.
If you're planning something similar, the AI architect page describes how I run these engagements, or analyse your challenge and bring the result to a call.
- 1JSON Schema specificationJSON Schema
- 2LLM01:2025 Prompt InjectionOWASP Gen AI Security Project
- 3Constraints (PostgreSQL documentation)PostgreSQL Global Development Group
- 4