Extend in Practice: Schemas, Evals and Workflows
Extend is built around one idea: an extraction configuration is a versioned artifact you test against ground truth before you ship it. Schemas, evaluation sets, and workflows are the three pieces of that loop, and they only make sense together.
This is part 4 of the Document AI series. It explains Extend's model from its documentation for API version 2026-02-09, read on September 26, 2026, and writes a schema for the same synthetic Harbor Lane invoice used in part 2. Nothing here was run against Extend. Outputs are the shapes the docs describe, filled in with the invoice's values.
The pieces and how they connect
Extend's agent context file is the fastest overview of the platform. It lists six capabilities: parse, extract, classify, split, edit for PDF forms, and workflows that chain them.
Four concepts carry most of the weight:
- Processor. A saved, versioned extractor, classifier, or splitter referenced by ID. Extractors use the
ex_prefix. - Version. Every processor has one editable draft and immutable published versions such as
1.0and1.1. A run can referencedraft,latest, or a pinned version. - Evaluation set. Files with expected outputs, attached to a processor. Running the set against a version returns accuracy metrics.
- Workflow. A graph of steps, from trigger to parse to extract to validation to human review to a webhook, itself versioned and deployed.
The docs recommend a promotion loop that ties these together: update the draft, run the evaluation set against the draft, publish, move the version string in your workflow step, then deploy the workflow.
A schema for the Harbor Lane invoice
Extend extractors take JSON Schema with a handful of house rules, listed in its schema docs. The root must be an object. Every primitive field must be nullable, written as a type array with null. Objects and arrays cannot be nullable. Nesting stops at five levels. Enums hold strings only and must include null. Composition keywords like anyOf and oneOf, regex patterns, and const are not supported.
Extend adds custom types on top. A date type returns yyyy-mm-dd. A currency type is an object with amount and iso_4217_currency_code. Here is a schema for the invoice that follows every rule:
{
"type": "object",
"properties": {
"invoice_number": {
"type": ["string", "null"],
"description": "The invoice number printed near the top, e.g. HLS-2026-0417"
},
"vendor_name": {
"type": ["string", "null"],
"description": "Legal name of the company that issued the invoice"
},
"po_number": {
"type": ["string", "null"],
"description": "The customer's purchase order number, if printed"
},
"invoice_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "The date the invoice was issued"
},
"due_date": {
"type": ["string", "null"],
"extend:type": "date",
"description": "The payment due date. Null if only terms are printed."
},
"document_type": {
"enum": ["invoice", "credit_note", null],
"extend:descriptions": ["A request for payment", "A credit or refund against an earlier invoice"]
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": ["string", "null"] },
"quantity": { "type": ["number", "null"] },
"unit_price": { "type": ["number", "null"] },
"amount": { "type": ["number", "null"] }
}
}
},
"subtotal": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"]
},
"tax": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"]
},
"total_due": {
"type": "object",
"extend:type": "currency",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"]
}
}
}
Three design choices in that schema are worth copying. due_date is allowed to be null, and its description says so, because an invoice that prints only "Net 30" should not get an invented date. document_type puts a credit note check inside extraction for a single-vendor feed, so a negative document cannot slip through as a charge. And the line items keep quantity, unit price, and amount separately, so a validation rule can recompute every line.
The docs also advise keeping the config in a file in your repository and submitting it from there, then fixing only the field path an error names. Extend normalizes what you submit, marking every property required and adding additionalProperties: false, so the stored config will not match your file byte for byte. That is expected.
What the output looks like
Extraction output splits into value, shaped like your schema, and metadata, keyed by field path. The response format docs and confidence score docs describe it. For the invoice, the shape is:
{
"value": {
"invoice_number": "HLS-2026-0417",
"invoice_date": "2026-09-01",
"due_date": "2026-10-01",
"line_items": [
{ "description": "Nitrile gloves, box of 100", "quantity": 20, "unit_price": 12.5, "amount": 250.0 }
],
"total_due": { "amount": 982.91, "iso_4217_currency_code": "USD" }
},
"metadata": {
"invoice_number": { "ocrConfidence": 0.99, "citations": ["..."] },
"line_items[0].amount": { "ocrConfidence": 0.97 }
}
}
The confidence numbers are placeholders. Two details in the docs change how you use them. ocrConfidence only appears when advancedOptions.citationsEnabled is true, so turn citations on if you plan to route on confidence. And logprobsConfidence is being phased out, returning null from extraction_performance version 4.6.0 onward. The docs point new integrations to ocrConfidence and to the optional Review Agent, which adds a 1 to 5 reviewAgentScore per field for an extra credit per page.
Extraction runs on one of two base processors. extraction_performance is the default and handles complex layouts and handwriting better. extraction_light is cheaper and faster, and drops figure parsing, signature detection, and page rotation, according to the configuration docs.
Evaluation sets: the part that makes this a platform
An evaluation set is a processor's answer key. The running evaluation sets docs describe the loop as a handful of API calls: create a set for a processor, upload files, add items with an expected output, start a run against a version, and poll until it finishes.
For an extractor, each item's expectedOutput nests the full correct object under value. The docs suggest the practical way to build one: run the extractor on the file, correct the output by hand, and save the corrected version. For the Harbor Lane invoice that means a reviewer confirms all ten fields and the three line items once, and that record then tests every future version.
A finished extraction run returns an overall accuracy and fieldMetrics per field path, with counts of total, present, expected, and accurate values. Per-field numbers are the useful ones. An overall 0.95 can hide a total_due that is wrong on a tenth of invoices.
Know where the API stops. The docs say the run object carries aggregate metrics only. Per-document diffs, CSV export, run-to-run comparison, excluding a field from scoring, and the choice of matcher live in Extend Studio. There are four matchers for string fields: strict equality, fuzzy matching with a threshold, an LLM judge you can give a rubric, and vector similarity. If you need per-document results in code, the docs say to run the processor on each file and diff against the expected output yourself, which is what part 7 of this series builds.
Workflows: validation and review around the extractor
A workflow wraps the extractor in steps that decide what happens to each document. The human review docs give the minimal definition:
{
"steps": [
{ "name": "trigger", "type": "TRIGGER", "next": [{ "step": "parse" }] },
{ "name": "parse", "type": "PARSE", "next": [{ "step": "extract" }] },
{
"name": "extract",
"type": "EXTRACT",
"config": { "extractor": { "id": "ex_abc123", "version": "latest" } },
"next": [{ "step": "review" }]
},
{ "name": "review", "type": "HUMAN_REVIEW", "next": [{ "step": "webhook" }] },
{ "name": "webhook", "type": "WEBHOOK_RESPONSE" }
]
}
That sends every document to a person. The useful version puts a Validation step between extract and review, with its "If All Valid" output going straight to the webhook and "If Any Invalid" going to review. Per the validation step docs, a code block runs JavaScript against upstream outputs and returns true, false, or false with a reason. A semantic block asks an LLM to judge plausibility instead.
For the Harbor Lane invoice, the first code block should reconcile the arithmetic. This one follows the documented context.outputs pattern, with the step named extract as above:
const inv = context.outputs.extract;
const lines = inv.line_items.value || [];
const lineSum = lines.reduce((sum, item) => sum + (item.amount || 0), 0);
const subtotal = inv.subtotal.value.amount;
const tax = inv.tax.value.amount;
const total = inv.total_due.value.amount;
if (subtotal == null || total == null) {
return [false, "Subtotal or total is missing"];
}
if (Math.abs(lineSum - subtotal) > 0.01) {
return [false, "Line items do not sum to the subtotal"];
}
if (Math.abs(subtotal + (tax || 0) - total) > 0.01) {
return [false, "Subtotal plus tax does not equal the total"];
}
return true;
The docs show scalar fields read as .value under the step name. The exact shape of array items and currency objects is not spelled out on that page, so confirm it in the Variables panel before relying on this block. The editor type-checks against the real shape.
On the synthetic invoice, 250.00 plus 240.00 plus 418.00 is 908.00, and 908.00 plus 74.91 is 982.91, so a correct extraction passes. A misread total fails with a reason a reviewer can act on.
When a run reaches review, its status becomes NEEDS_REVIEW and a workflow_run.needs_review webhook fires. The operator corrects and approves in the dashboard. There is no API to approve on someone's behalf. Reviewed step outputs carry initialOutput and reviewedOutput, so your system can tell a machine value from a corrected one. Keep both. Those corrections are free ground truth for the next evaluation set, and part 8 of the series designs the queue around them.
One versioning trap from the workflow configuration docs: extract steps accept latest, but latest resolves at run time, so publishing a new extractor version changes a deployed workflow's behavior. Classify and split steps refuse latest for that reason. For a reproducible production workflow, pin the extractor version too.
What it costs, as of September 26, 2026
Extend bills in credits, per its credit docs. Pay as you go starts with 10,000 free credits, then $0.0125 per credit. Scale is $500 a month with 50,000 credits included. Performance extract costs 3 credits per page and its automatic parse costs 2, so the all-in rate the docs give is $0.0625 per page. Light extract with light parse is 1.2 credits, or $0.015 per page. The Review Agent adds 1 credit per page.
Those rates are presets in the document processing cost calculator, which adds the review time from the workflow above. Use test-mode API keys while you build: the test environment guide says they route to an isolated environment with no production data or webhooks.
Where this leaves you
Extend asks for more setup than a single extract call: a schema file, an evaluation set, a workflow with rules. That is also its strongest argument. The same scaffolding is what evaluating an agent requires, and here most of it is built in.
Previous: Datalab in practice: Marker, Surya and Chandra, then the API. Next: Extend vs Datalab vs other document parsers.
