Skip to content
Zarif Automates

Extend in Practice: Schemas, Evals and Workflows

ZarifZarif
Published Updated

Say you set up an extractor that pulls the total off every invoice. It works. A month later you tweak one field description, and now 1 in 10 totals comes back wrong. Nobody notices until the books don't balance.

Extend is built to stop that. You save each version of your setup, test it against documents where you already know the right answers, and only then switch to it. Three pieces make that work: schemas, evaluation sets and workflows. They only make sense together.

This is part 4 of the Document AI series. It explains how Extend works from its documentation for API version 2026-02-09, read on September 26, 2026. It also writes a schema for the same made-up Harbor Lane invoice used in part 2. Nothing here was run against Extend. The outputs shown are the shapes the docs describe, filled in with the invoice's values.

The pieces and how they fit

Extend's agent context file is the quickest overview of the platform. It lists six things it does: parse, extract, classify, split, fill in PDF forms, and workflows that chain the others together.

Four ideas do most of the work:

  • Processor. A saved extractor, classifier or splitter, looked up by its ID. Extractor IDs start with ex_.
  • Version. Every processor has one draft you can edit, plus published versions like 1.0 and 1.1 that never change. A run can use draft, latest, or one specific version.
  • Evaluation set. A set of files paired with the right answers, attached to a processor. Run a version against it and you get back accuracy numbers. Think of it as an answer key.
  • Workflow. A chain of steps: a document comes in, gets parsed, gets extracted, gets checked, maybe goes to a person, then gets sent on to your system. Workflows have versions too.

The docs recommend a routine that ties these together. Edit the draft. Run the evaluation set against it. Publish it as a new version. Point your workflow at the new version number. Then deploy the workflow.

A schema for the Harbor Lane invoice

A schema is the list of fields you want back, what type each one is, and a short description that tells the model what to look for. Extend uses JSON Schema with a few house rules, listed in its schema docs:

  • The top level has to be an object.
  • Every simple field (text, number, date) has to allow null, written as a type list that includes null.
  • Objects and arrays can't be null.
  • You can nest at most five levels deep.
  • Fixed lists of choices (enums) can only hold text, and must include null.
  • anyOf, oneOf, regex patterns and const aren't supported.

Extend adds its own types on top. A date type returns yyyy-mm-dd. A currency type is an object with an amount and an iso_4217_currency_code, like USD. Here's 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 choices in that schema are worth copying.

  • due_date is allowed to be null, and its description says so. An invoice that only says "Net 30" shouldn't get a made-up date.
  • document_type asks whether this is a credit note. If all your documents come from one vendor, that keeps a refund from slipping through as a charge.
  • Line items keep quantity, unit price and amount separate. That way a checking rule can redo the math on every line.

The docs also suggest keeping the schema in a file in your code repository and sending it from there. When you get an error, fix only the field it names.

Don't be surprised when the saved version looks different from your file. Extend tidies up what you send: it marks every field required and adds additionalProperties: false. That's expected.

What the output looks like

The output comes in two parts. value holds the answers, shaped like your schema. metadata holds extra details for each field, keyed by the field's path. The response format docs and confidence score docs describe it. For the invoice, it looks like this:

{
  "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 there are placeholders. Two details in the docs change how you use them.

First, ocrConfidence only shows up when advancedOptions.citationsEnabled is true. If you plan to send low-confidence fields to a person, turn citations on.

Second, logprobsConfidence is being retired. It returns null from extraction_performance version 4.6.0 onward. The docs point new projects to ocrConfidence, and to the optional Review Agent, which gives each field a score from 1 to 5 (reviewAgentScore) for an extra credit per page.

Extraction runs on one of two base engines, per the configuration docs. extraction_performance is the default and does better with complicated layouts and handwriting. extraction_light is cheaper and faster, but drops figure reading, signature detection and page rotation.

Evaluation sets: the answer key

An evaluation set is the answer key for a processor. The running evaluation sets docs lay it out as a few API calls: create a set for a processor, upload files, add each one with its correct output, start a run against a version, and check back until it's done.

For an extractor, each item's expectedOutput holds the full correct answer under value. The docs suggest an easy way to build one. Run the extractor on the file, fix its output by hand, and save the fixed version.

For the Harbor Lane invoice, that means someone checks all ten fields and the three line items once. From then on, that record tests every future version.

A finished run gives you an overall accuracy and a fieldMetrics breakdown for each field, with counts of total, present, expected and accurate values. The per-field numbers are the ones to watch. An overall score of 0.95 can hide a total_due that's wrong on 1 in 10 invoices.

Know where the API stops. The docs say a run only gives you the summary numbers through the API. These all live in Extend Studio, the web dashboard, instead:

  • seeing differences document by document
  • exporting to CSV
  • comparing one run to another
  • leaving a field out of the score
  • choosing how text fields get compared

There are four ways to compare text fields: an exact match, a close-enough match with a threshold, an LLM that grades the answer against a rubric you write, and a similarity score. If you need per-document results in code, the docs say to run the processor on each file and compare it to the right answer yourself. Part 7 of this series builds exactly that.

Workflows: checks 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 simplest version:

{
  "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 single document to a person. The useful version adds a Validation step between extract and review. If everything passes ("If All Valid"), the document goes straight to the webhook. If anything fails ("If Any Invalid"), it goes to review.

According to the validation step docs, there are two kinds of check. A code block runs JavaScript on the earlier steps' output and returns true, false, or false with a reason. A semantic block asks an LLM whether the values look believable.

For the Harbor Lane invoice, the first code block should check the math. 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;

One caution. The docs show simple fields read as .value under the step name, but that page doesn't spell out the exact shape of list items and currency objects. Check it in the Variables panel before you rely on this block. The editor checks your code against the real shape.

On the made-up invoice, 250.00 plus 240.00 plus 418.00 is 908.00, and 908.00 plus 74.91 is 982.91. A correct extraction passes. A misread total fails, with a reason a reviewer can act on.

When a document goes to review, its status changes to NEEDS_REVIEW and a workflow_run.needs_review webhook fires. A person fixes and approves it in the dashboard. There's no API to approve on someone's behalf.

Reviewed steps keep both initialOutput and reviewedOutput, so your system can tell what the machine said from what a person corrected. Keep both. Those corrections are free right answers for your next evaluation set. Part 8 of the series designs the review queue around them.

There's one version trap, from the workflow configuration docs. Extract steps accept latest, but latest is looked up each time a document runs. So publishing a new extractor version quietly changes a workflow that's already live. Classify and split steps refuse latest for exactly that reason. If you want production to behave the same way every day, lock the extractor to a specific version too.

What it costs, as of September 26, 2026

Extend charges 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 a page, and its automatic parse costs 2, so the all-in price the docs give is $0.0625 a page.
  • Light extract with light parse is 1.2 credits, or $0.015 a page.
  • The Review Agent adds 1 credit a page.

Those prices are built into the document processing cost calculator, which adds the cost of the review time from the workflow above. Use test-mode API keys while you build. The test environment guide says they run in a separate environment with no production data or webhooks.

Where this leaves you

Extend takes more setup than a single extract call: a schema file, an answer key, and a workflow with checks. That's also the best reason to use it. It's the same setup evaluating an agent needs, and here most of it comes built in.

Next in the series: Extend vs Datalab vs other document parsers. The previous part is Datalab in practice: Marker, Surya and Chandra, then the API.

Zarif Choudhury

Zarif Choudhury

Zarif builds AI agents and automation workflows. He writes about what keeps working once real people use it: who is worth reading, the new jobs AI is creating, and agent workflows you can check step by step.