Schema Validation & Repair diagramInvoice, Extractor, Validator, Typed data, Rejected. Step captions follow the diagram.InvoiceExtractorValidatorschema + rulesTyped dataRejectedSchema Validation & Repair diagramInvoice, Extractor, Validator, Typed data, Rejected. Step captions follow the diagram.InvoiceExtractorValidatorschema+ rulesTyped dataRejected

All steps

Repaired once

Gives up after 3 tries

Parts of this pattern
Invoice Input / output
The input to extract from: here, the OCR text of a scanned supplier invoice dated 2026-03-02.
Extractor LLM call
Reads the document and replies with JSON for vendor, total, currency and due_date; on a repair round it also sees its last reply and the errors.
Validator Guard
Plain code: parses the JSON, checks types and required fields, then runs business rules such as 'due_date is after the invoice date'.
Typed data Input / output
The validated object, safe to hand to accounting code that expects a real date and a decimal total.
Rejected Input / output
After the last attempt fails, the item and its errors go to a review queue instead of into the database.
Invoice → Extractor
The document and the schema go to the model with the extraction prompt.ACME Tools Ltd. Invoice 4471, dated 2 March 2026. Total due: 1,240.00 EUR. Payment due next Friday.
Extractor → ValidatorJSON
The model's reply goes to the validator as raw text, before any code trusts it.{"vendor": "ACME Tools Ltd.", "total": 1240.00, "currency": "EUR", "due_date": "next Friday"}
Validator → Extractorerror
On failure, the validator's exact error messages go back to the model with its previous reply, so it knows which field to fix.due_date: 'next Friday' is not a valid date (expected YYYY-MM-DD, after invoice date 2026-03-02)
Validator → Typed datavalid
When every check passes, the parsed object leaves as typed data.{"vendor": "ACME Tools Ltd.", "total": 1240.00, "currency": "EUR", "due_date": "2026-03-06"}
Validator → Rejected3rd failure
If the third attempt still fails, the loop stops and raises an error carrying the last reply and its failures.ValidationError after 3 attempts: total: -1240.0 must be positive. Sent to review queue with invoice 4471.

← → neighboursesc atlas

Reliability & Runtime

Schema Validation & Repair

Also known as Validate-and-reask · Structured output retry · Output parser with repair · Instructor reask

Check the model's output against a schema and rules; on failure, send the exact error back and ask again, up to N tries.

No single inventor · see source

No single inventor: the same validate-and-reask loop ships as LangChain's OutputFixingParser and in Guardrails AI. Jason Liu's Instructor (repository started as jxnl/instructor in June 2023) made it popular.

Measured over one run

Nodes
5. Boxes in the diagram: models, tools, stores, people and plain code.
LLM calls
2. Model invocations in the first scenario: each time a model or agent node starts thinking, plus each branch it decides.
Tool calls
0. Messages sent to a tool node in the first scenario.
Steps
3. Steps in the first scenario, with repeated steps counted every time they run.
Scenarios
2. Distinct example runs you can switch between on the diagram.
Loop
yes. Whether the flow cycles back on itself until a condition is met.
Parallel
no. Whether two or more messages are in flight at the same time.
Branch
yes. Whether the flow takes one path out of several, chosen at run time.
Human in the loop
no. Whether a person appears as a node, to approve, correct or answer.
Autonomy
low. How much the model decides the control flow itself, from fixed code paths (low) to open-ended (high).
Cost
low. Relative token spend per request.
Latency
medium. Relative wall-clock time per request.

Counted from the first scenario's timeline when the site is built.

Problem

Code downstream of a model needs data it can trust: a real date, a positive decimal, a currency from a fixed list. Models usually return the right shape, but not always. Asked to extract an invoice dated 2 March, a model may copy the due date as the words “next Friday”, drop a required field, or wrap the JSON in a sentence. JSON mode and constrained decoding fix the syntax, but they cannot enforce rules such as “the due date is after the invoice date”. One bad record that slips through ends up in the ledger.

How it works

The extractor reads the invoice and replies with JSON. Before any code trusts that reply, a validator parses it, checks types and required fields against the schema, and runs the business rules. The validator is plain code, not a model, so its verdict is deterministic.

If everything passes, the parsed object leaves as typed data. If anything fails, the validator’s exact error messages go back to the model along with its previous reply: “due_date: ‘next Friday’ is not a valid date (expected YYYY-MM-DD, after invoice date 2026-03-02)”. That message tells the model which field is wrong and why, so the second try is usually right. In the example the model resolves “next Friday” to 2026-03-06 and the record passes.

The loop has a hard cap, typically three attempts. If the last one still fails, the item goes to a review queue with its errors rather than into the database.

The specific error is what makes this cheap. A generic “that was invalid, try again” wastes a call; a field path plus the rule that broke usually fixes the output in one round.

When to use

  • Downstream code needs typed data: records for a database, arguments for an API, rows for a report.
  • You have rules a schema cannot express, such as ranges, cross-field checks or totals that must add up.
  • Occasional bad outputs are expected and one extra call is cheaper than a human cleaning up later.

When to avoid

  • The provider’s constrained decoding already enforces every constraint you have; the loop would never fire.
  • The output is free text for a person to read. There is no schema to check against.
  • The rule needs judgement (“is this summary accurate?”). That is an evaluator’s job, not a validator’s.

Trade-offs

Most calls pass on the first try and cost nothing extra. The failures cost one more call each, and they are the inputs most likely to be ambiguous, so a repaired value can still be a guess. The strictness of the rules sets the balance: loose rules let bad data through; overly strict rules burn retries on records that were fine.

Failure modes

  • Plausible repair: the model invents a value just to satisfy the rule, such as a made-up date. Allow null for missing fields and test that the extractor uses it.
  • Fixing one, breaking another: the repaired reply drops a field that was correct. Send every error in one call and tell the model to keep the other fields unchanged.
  • Silent give-up: after the cap, the code stores the last reply anyway. Raise an error and route the item to review instead.
  • Vague errors: a raw stack trace or “invalid input” gives the model nothing to act on. Report each failure as a field path plus a plain message.

Build it with your AI

Paste this into Claude Code, Cursor, ChatGPT or any AI coding assistant.

Implement the Schema Validation & Repair pattern in this codebase, using the LLM client and language we already use.

## Goal

Turn unstructured input into an object that passes a typed schema and every business rule, feeding the exact validation error back to the model until it passes or a retry budget runs out.

## How it works

1. Define the target as a schema with types and required fields, plus named rule functions for checks a schema cannot express (ranges, dates in order, totals that add up).
2. Call the model with the Extraction prompt, filling in the input and the schema rendered as JSON Schema.
3. Parse the reply as JSON, then validate it against the schema and run every rule. Collect each failure as a field path plus a message.
4. If there are no failures, return the typed object.
5. Otherwise call the model with the Repair prompt, passing the input, the previous reply and the list of failures verbatim, and validate the new reply the same way.

## Prompts

### Extraction prompt

```markdown
Extract the fields below from the document. Reply with one JSON object that
matches this schema exactly, and nothing else.

<schema>{{json_schema}}</schema>
<rules>{{rules}}</rules>
<document>{{document}}</document>

If a field is missing from the document, use null rather than guessing.
```

### Repair prompt

```markdown
Your previous reply failed validation. Fix every error listed below and reply
with the corrected JSON object only.

<schema>{{json_schema}}</schema>
<rules>{{rules}}</rules>
<document>{{document}}</document>
<previous_reply>{{previous_reply}}</previous_reply>
<errors>
{{errors}}
</errors>

Keep every field that had no error exactly as it was.
```

## Stop rule

Stop when a reply passes validation, or after 3 attempts in total. If the budget runs out, raise a validation error that carries the last reply and its failures, so the caller can queue the item for a person instead of storing bad data.

## Requirements

- Make the attempt cap a parameter (default 3); never loop without one.
- Treat a reply that is not valid JSON as a validation failure whose message is the parser's error, not as a crash.
- Send every failure in one repair call, each with its field path, so the model can fix them all at once.
- Log each attempt with the raw reply, the failures and the token counts.
- Add a test with a stub LLM whose first reply has due_date "next Friday" and whose second has a valid date; assert two model calls, that the repair prompt contains the text "due_date", and that the returned object has the second date.

Prompt templates

Extraction prompt
Extract the fields below from the document. Reply with one JSON object that
matches this schema exactly, and nothing else.
<schema>{{json_schema}}</schema>
<rules>{{rules}}</rules>
<document>{{document}}</document>
If a field is missing from the document, use null rather than guessing.
Repair prompt
Your previous reply failed validation. Fix every error listed below and reply
with the corrected JSON object only.
<schema>{{json_schema}}</schema>
<rules>{{rules}}</rules>
<document>{{document}}</document>
<previous_reply>{{previous_reply}}</previous_reply>
<errors>
{{errors}}
</errors>
Keep every field that had no error exactly as it was.

Related patterns

Composes well with:

Sources

  1. Instructor docs: Retry Logic with Tenacity
  2. Instructor on GitHub (started by Jason Liu as jxnl/instructor)

From the Agentic Atlas by Joran Kikke · Edit on GitHub · Suggest a correction