extractInvoice function that returns the fields Extract found, and a triage function that decides whether each invoice can post.
Outcome: Consistent invoices post with every field filled in. An invoice with a missing field or totals that don’t add up goes to review with the reason attached. Running the same invoice twice gives the same fields.
1. Describe the fields
Give each field a name and a one-line description of what to look for. Extract uses the description to find the field and labels what it finds with the name, so pick names that work as keys, likeinvoice_number. The wording matters. “With currency” on the amounts makes Extract include the currency code or symbol, which step 3 needs to compare currencies and to read three-decimal currencies like the Kuwaiti dinar correctly.
2. Call Extract
Send the invoice text as the user message. Field descriptions go inprovider.options (the part of an OpenRouter request that carries provider-specific settings), under scaledown.entities. Extract replies with a JSON string in message.content. Inside is an entities array with one entry per field it found: the field name as type, the matching invoice text as text, the start and end character positions of that text in your input, and a confidence. The code parses the string and validates it with Zod before using it.
entities array Extract returned for this invoice, reordered to match INVOICE_ENTITIES. start and end index into INVOICE, so INVOICE.slice(8, 30) is the vendor. The response also carries fields this recipe doesn’t use, such as ocr_text and a context snippet for each entity.
entities. Read confidence as “a match was found”, not as the chance the value is right, and don’t route invoices on it.
3. Check the invoice before you post it
triage takes the first value Extract found for each field, checks that none are missing, and checks that subtotal plus tax equals the total in one currency. parseMoney reads each amount as whole cents (or fils, for three-decimal currencies like the Kuwaiti dinar) and refuses anything it can’t read unambiguously, such as a negative amount. Those invoices go to review.
problems list can post. Anything else goes to review, with problems naming what failed. Dates come back exactly as printed (2026-10-30, October 15, 2026, 31.10.2026, and 20 Oct 2026 in our runs), so convert due_date with your date library before it reaches the ledger.
Worked example
The six made-up invoices below cover the layouts an accounts-payable queue sees: the table from step 2, a prose paragraph, a German-English mix with12.400,00 EUR amounts, a one-line statement with fields separated by pipes, an invoice whose printed total is $10 too high, and a freight invoice with no tax line. Save steps 1 to 3 and this block as one file and run it with bun run invoices.ts. Step 2’s own call runs first and prints the Northwind entities array; the loop then prints one line per invoice.
Check your work
- Request. The request body carries
provider.options.scaledown.entitieswith one description per field. - Response.
message.contentparses as JSON with anentitiesarray. Each entry hastype,text,confidence,start, andend. Eachtextequalsinvoice.slice(start, end). - Clean invoice. Every field in
INVOICE_FIELDSis present andtriagereturnspost: true. - One cent off. Change the total by one cent, on a small invoice and on a large one. Both return
totals_mismatch. - No cents.
$1,000/$80/$1,080posts, with the comma read as a thousands separator. - Three-decimal currency.
KWD 1.250/KWD 0.100/KWD 1.350posts, and so doesKWD 1.250/KWD 0.25/KWD 1.500(1,250 + 250 = 1,500 fils). A total ofKWD 1.351returnstotals_mismatch, andKWD 3,000anywhere returnsunparsed_amount. - Mixed currencies. Print the tax as
EUR 20.00on aUSDinvoice.problemscontainscurrency_mismatch. - Negative amount. Make the subtotal
-$100.00or($100.00).problemscontainsunparsed_amount. - Missing field. Remove the tax line.
problemscontainsmissing_vatandpostisfalse. - Stable. Run one invoice five times.
fieldsis identical every time.
Variations
ChangeINVOICE_ENTITIES and the same code pulls fields out of other documents: renewal terms, notice periods, and liability caps from contracts, guidance and segment revenue from filings, or error codes and product versions from support tickets. Whenever two extracted values must agree, keep a check like step 3.
Next steps
- Detect Stuck and Looping Agents with ScaleDown Classify to ask a model typed questions about an agent’s transcript
- Compact Long Agent Sessions with ScaleDown Summarize to summarize long agent histories