# Extend in Practice: Schemas, Evals and Workflows

> How Extend's extraction schemas, evaluation sets and workflows fit together, from its docs, with a worked schema for one synthetic invoice.

- Source: https://www.zarifautomates.com/blog/extend-in-practice-schemas-evals-workflows
- Published: 2026-09-26
- Updated: 2026-09-26
- Pillar: Agents & AI Engineering
- Tags: document ai, document-extraction, intelligent document processing, ai invoice processing workflow
- Author: Zarif

---

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](/blog/parse-vs-extract-vs-classify-vs-split). 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](https://docs.extend.ai/agents.md) 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.0` and `1.1`. A run can reference `draft`, `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](https://docs.extend.ai/extraction/schema). 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:

```json
{
  "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](https://docs.extend.ai/extraction/response-format) and [confidence score docs](https://docs.extend.ai/extraction/confidence-scores) describe it. For the invoice, the shape is:

```json
{
  "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](https://docs.extend.ai/extraction/configuration).

## Evaluation sets: the part that makes this a platform

An evaluation set is a processor's answer key. The [running evaluation sets docs](https://docs.extend.ai/evaluation/running-evaluation-sets) 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](https://docs.extend.ai/workflows/human-review) give the minimal definition:

```json
{
  "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](https://docs.extend.ai/workflows/workflow-steps/validation-step), 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:

```javascript
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](https://docs.extend.ai/workflows/configuring-workflows): 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](https://docs.extend.ai/general/how-credits-work). 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](/lab/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](https://docs.extend.ai/general/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](/blog/evaluate-ai-agent-performance-reliability) requires, and here most of it is built in.

Previous: [Datalab in practice: Marker, Surya and Chandra, then the API](/blog/datalab-in-practice-marker-surya-chandra). Next: [Extend vs Datalab vs other document parsers](/blog/extend-vs-datalab-vs-other-document-parsers).


