object field captures the line-item table, so each row carries its own values, confidence and evidence.
The workflow
{
"name": "Invoice processing",
"description": "Extract the invoice header and its line items",
"nodes": [
{ "id": "parse_1", "type": "parse" },
{
"id": "extract_1",
"type": "extract",
"extraction_schema": {
"fields": [
{ "name": "invoice_number", "description": "The unique invoice identifier", "data_type": "string" },
{ "name": "vendor_name", "description": "Name of the company that issued the invoice", "data_type": "string" },
{ "name": "issue_date", "description": "Date the invoice was issued", "data_type": "date" },
{ "name": "due_date", "description": "Date by which payment is due", "data_type": "date" },
{ "name": "subtotal", "description": "Amount before tax", "data_type": "float" },
{ "name": "tax_amount", "description": "Total tax amount", "data_type": "float" },
{ "name": "total_amount", "description": "Final total amount due including tax", "data_type": "float" },
{
"name": "currency",
"description": "Currency of the invoice amounts",
"data_type": "enum",
"enum_options": [
{ "name": "USD", "description": "US Dollar" },
{ "name": "EUR", "description": "Euro" },
{ "name": "GBP", "description": "British Pound" }
]
},
{
"name": "line_items",
"description": "Individual line items listed on the invoice, one row per item",
"data_type": "object",
"nested_fields": [
{ "name": "description", "description": "Description of the item or service", "data_type": "string" },
{ "name": "quantity", "description": "Number of units", "data_type": "integer" },
{ "name": "unit_price", "description": "Price per unit", "data_type": "float" },
{ "name": "amount", "description": "Total amount for this line item", "data_type": "float" }
]
}
]
}
}
],
"edges": [{ "source": "parse_1", "target": "extract_1" }]
}
import os
from anyformat.sdk import Client
from anyformat.workflow import Schema
client = Client(api_key=os.environ["ANYFORMAT_API_KEY"])
workflow = (
client.workflow("Invoice processing", "Extract the invoice header and its line items")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.string("vendor_name", "Name of the company that issued the invoice"),
Schema.date("issue_date", "Date the invoice was issued"),
Schema.date("due_date", "Date by which payment is due"),
Schema.float("subtotal", "Amount before tax"),
Schema.float("tax_amount", "Total tax amount"),
Schema.float("total_amount", "Final total amount due including tax"),
Schema.enum("currency", "Currency of the invoice amounts", options=[
Schema.option("USD", "US Dollar"),
Schema.option("EUR", "Euro"),
Schema.option("GBP", "British Pound"),
]),
Schema.object("line_items", "Individual line items listed on the invoice, one row per item", fields=[
Schema.string("description", "Description of the item or service"),
Schema.integer("quantity", "Number of units"),
Schema.float("unit_price", "Price per unit"),
Schema.float("amount", "Total amount for this line item"),
]),
])
.create()
)
import { Anyformat, Schema } from "@anyformat/sdk";
const af = new Anyformat({ apiKey: process.env.ANYFORMAT_API_KEY! });
const workflow = await af
.workflow("Invoice processing", "Extract the invoice header and its line items")
.parse()
.extract([
Schema.string("invoice_number", "The unique invoice identifier"),
Schema.string("vendor_name", "Name of the company that issued the invoice"),
Schema.date("issue_date", "Date the invoice was issued"),
Schema.date("due_date", "Date by which payment is due"),
Schema.float("subtotal", "Amount before tax"),
Schema.float("tax_amount", "Total tax amount"),
Schema.float("total_amount", "Final total amount due including tax"),
Schema.enum("currency", "Currency of the invoice amounts", [
Schema.option("USD", "US Dollar"),
Schema.option("EUR", "Euro"),
Schema.option("GBP", "British Pound"),
]),
Schema.object("line_items", "Individual line items listed on the invoice, one row per item", [
Schema.string("description", "Description of the item or service"),
Schema.integer("quantity", "Number of units"),
Schema.float("unit_price", "Price per unit"),
Schema.float("amount", "Total amount for this line item"),
]),
])
.create();
Run it and read the result
Upload the invoice, then poll the run until itsstatus is processed. The results arrive inline on that same read.
# 1. Create the workflow with the JSON above
curl -X POST 'https://api.anyformat.ai/v3/workflows/' \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-H 'Content-Type: application/json' \
-d @invoice-workflow.json
# → 201 { "id": "<workflow_id>", ... }
# 2. Upload the invoice and start a run
curl -X POST "https://api.anyformat.ai/v3/workflows/$WORKFLOW_ID/upload/run/" \
-H "Authorization: Bearer $ANYFORMAT_API_KEY" \
-F 'files=@invoice.pdf'
# → 202 { "run_id": "<run_id>", "document_packet_id": "...", "status": "queued" }
# 3. Poll until "status" is "processed"
curl "https://api.anyformat.ai/v3/runs/$RUN_ID/" \
-H "Authorization: Bearer $ANYFORMAT_API_KEY"
result = workflow.run("invoice.pdf").wait()
print(result.fields["invoice_number"].value)
print(result.fields["total_amount"].value)
# An object field is a list of rows; each cell is an ExtractedField.
for row in result.fields["line_items"]:
print(row["description"].value, row["quantity"].value, row["amount"].value)
import { readFileSync } from "node:fs";
import type { ExtractedField, ExtractedRows } from "@anyformat/sdk";
const file = new File([readFileSync("invoice.pdf")], "invoice.pdf", { type: "application/pdf" });
const result = await (await workflow.run(file)).wait();
console.log((result.field("invoice_number") as ExtractedField<string> | undefined)?.value);
console.log((result.field("total_amount") as ExtractedField<string> | undefined)?.value);
// An object field comes back as rows; each cell is an ExtractedField.
for (const row of result.field("line_items") as ExtractedRows) {
console.log(row.description?.value, row.quantity?.value, row.amount?.value);
}
Response
Theresults envelope of a processed run, trimmed to the interesting fields. Every scalar field carries value, confidence, evidence, verification_status and value_override; the line_items rows repeat that shape per cell.
{
"id": "06a8d8f5-1f88-7513-8000-637f6625c2c6",
"status": "processed",
"results": {
"document_packet_id": "06a8d8f5-1dbc-7497-8000-d61ff7a5fecf",
"parse": { "markdown": "<a id=\"p1_b0\"></a>\n\n# INVOICE\n\n...", "parse_confidence": 98.0, "blocks": [] },
"classifications": [],
"splits": [],
"extractions": [
{
"split_name": null,
"partition": null,
"fields": {
"invoice_number": {
"value": "INV-2026-9001",
"confidence": 98.0,
"evidence": [{ "text": "Invoice #: INV-2026-9001", "page_number": 1 }],
"verification_status": "not_verified",
"value_override": null
},
"total_amount": {
"value": "4594.62",
"confidence": 98.0,
"evidence": [{ "text": "**TOTAL: $4,594.62**", "page_number": 1 }],
"verification_status": "not_verified",
"value_override": null
},
"line_items": [
{
"description": { "value": "Cloud hosting, March", "confidence": 97.0, "evidence": [], "verification_status": "not_verified", "value_override": null },
"quantity": { "value": "1", "confidence": 97.0, "evidence": [], "verification_status": "not_verified", "value_override": null },
"amount": { "value": "1200.00", "confidence": 97.0, "evidence": [], "verification_status": "not_verified", "value_override": null }
}
]
}
}
],
"edits": []
}
}
Tips
- Write specific field descriptions. “Final total amount due including tax” extracts better than “total”.
- Numbers arrive as strings in the JSON body (
"4594.62"). Cast them with the type you declared. - List every currency you expect in
enum_options. Anenumconstrains the output to those names. - Multi-page invoices need no configuration. The evidence
page_numbertells you where each value came from. - To check that
subtotal + tax_amount = total_amount, add a Validate node with an arithmetic rule. See Contract analysis for a graph with validation.
Next steps
Field types
Object, enum and the other field shapes
Runs and results
Every section of the results envelope

