Cookbook: Invoices
Cookbook: invoices
Section titled “Cookbook: invoices”The job: turn an invoice PDF (or photo of one) into a structured record — number, dates, vendor, customer, line items, tax, totals.
The recipe
Section titled “The recipe”from scriva import Orchestrator, Workerfrom scriva.prompts import Prompt
worker = ( Worker.openai("gpt-4o") .prompt(Prompt.invoice) .cache(".scriva_cache") .score(method="rendering") .retry(times=3))
recipe = ( Orchestrator() .orient() .deskew() .split.boxes(detector="invoice") # vendor block + items table + totals .classify(blank=True, merged=True) .recognize.by_kind({ "text": worker, "number": Worker.number(), "date": Worker.date(), "blank": Worker.skip(), }) .reconstruct.kv() .export.json("acme.json"))
result = recipe("acme-2026-05.pdf")recipe.to_dict()["fields"] gives you the typed record. Vendor names,
line items, tax/total relationships — every cell that the splitter
found has a Recognition with confidence. Use Worker.number() for
amounts and Worker.date() for the issue/due dates and they parse to
ISO and Decimal for free.
What you get back
Section titled “What you get back”The standard invoice record:
class Invoice(BaseModel): invoice_number: str invoice_date: date due_date: date | None currency: str # ISO 4217 ("USD", "EUR", "JPY") subtotal: Decimal | None tax_amount: Decimal | None total_amount: Decimal vendor: Party customer: Party | None line_items: list[LineItem] payment_terms: str | None notes: str | NoneFull type catalogue: Schemas › Invoice.
Customising
Section titled “Customising”Add fields specific to your vendors
Section titled “Add fields specific to your vendors”Subclass and the recipe’s reconstruct/export shape will surface the extra fields automatically:
class MyInvoice(Invoice): purchase_order: str | None = None project_code: str | None = None requisitioner_email: str | None = None
recipe = recipe.replace( "reconstruct", reconstruct.kv(schema=MyInvoice),)Vendor-name normalisation
Section titled “Vendor-name normalisation”Build a YAML dictionary of canonical vendor names and slot it in as a reconstruct step:
vendors: - canonical: "Acme Industries, Inc." aliases: ["Acme Ind.", "ACME INDUSTRIES", "Acme Inc"] - canonical: "MegaCorp PLC" aliases: ["MEGACORP", "Mega Corp", "MegaCorp Ltd."]recipe = recipe.insert_after( "recognize", reconstruct.dictionary.from_yaml( "vendors.yaml", apply_to="vendor.name", case_sensitive=False, ),)Multi-currency inference
Section titled “Multi-currency inference”Pin a default for invoices that do not print a currency code:
recipe = recipe.replace( "recognize", Worker.router({ "number": Worker.number().with_language("en").hint("default currency USD"), "date": Worker.date(), "text": worker, }),)Worker.number() recognises symbols and ISO codes when present; the
static hint only kicks in if the crop has neither.
Tax sanity-check
Section titled “Tax sanity-check”A small reconstruct step that flags arithmetic mismatches:
from decimal import Decimalfrom scriva import step
@step(phase="reconstruct", name="tax-sanity")async def tax_sanity(ctx): inv = ctx.fields expected = (inv.subtotal or Decimal(0)) + (inv.tax_amount or Decimal(0)) if inv.total_amount and abs(expected - inv.total_amount) > Decimal("0.02"): inv.notes = (inv.notes or "") + " [WARN: subtotal+tax != total]" return ctx
recipe = recipe.insert_after("recognize", tax_sanity)Production notes
Section titled “Production notes”Cache, always
Section titled “Cache, always”worker = worker.cache(".scriva_cache")Invoice templates from the same vendor look almost identical month
over month. Pair an exact + semantic cache via
Cache.layered — 30–60% hit rates on real-world vendor
mixes are routine, and invoice runs are where the cache pays for
itself fastest.
Cost cap per invoice
Section titled “Cost cap per invoice”recipe = recipe.options(max_cost_usd=0.10)result = recipe("huge.pdf")Raises BudgetExceeded if a single invoice exceeds your per-doc cap;
the partial result is attached to the exception so you can ledger it.
Retries on the Worker
Section titled “Retries on the Worker”worker = worker.retry(times=4, backoff="expo")VLM 429s and transient 5xx are retried; auth and validation errors are not. See Reliability.
Batch with idempotent restart
Section titled “Batch with idempotent restart”import scriva
results = scriva.batch( sources=Path("incoming/").glob("*.pdf"), recipe=recipe, max_concurrency=8, on_error="continue", resume_file=".scriva_invoices.json",)Re-running picks up where it left off; partial results flush every
N items (flush_every=10 default).
Confidence-gated auto-post
Section titled “Confidence-gated auto-post”result = recipe("acme.pdf")if result.mean_confidence >= 0.92 and not result.low_confidence(0.7): post_to_erp(result.to_dict()["fields"])else: queue_for_human_review(result)low_confidence(0.7) returns regions below that confidence — empty
means the run is all-green and safe to auto-post.
HITL when confidence is not enough
Section titled “HITL when confidence is not enough”recipe = recipe.review.hitl( when=lambda r: (r.confidence or 0) < 0.7, sidecar="review.json",).review.hitl(...) pauses the recipe; the sidecar JSON matches the
shape of result.to_dict() so a UI can round-trip edits losslessly.
See Orchestrator › Review.
When the recipe needs more shape
Section titled “When the recipe needs more shape”Drop down further — every step is replaceable:
recipe = ( Orchestrator() .orient() .deskew() .split.boxes(detector="invoice") .classify(blank=True, merged=True) .recognize( Worker.escalate( primary=Worker.openai("gpt-4o-mini") .cache(".scriva_cache") .score(method="rendering"), oracle=Worker.openai("gpt-4o"), when=lambda r: (r.confidence or 0) < 0.7, ) ) .reconstruct.kv(schema=MyInvoice) .export.json("out/{stem}.json") .export.xlsx("out/{stem}.xlsx"))Cheap-first / oracle-second on a per-cell basis; two exports in one run. Every method swaps cleanly into the chain.
What to read next
Section titled “What to read next”- Worker — engines, decorators, composition.
- Orchestrator — steps, options, events.
- Cookbook › Batch & watch — folder ingestion.
- Reliability — production retries, rate limits, cost caps.
- Evaluation — score your recipe against ground truth.