Document intake
Invoices and contracts arrive as PDFs and scans. Millstream fetches each document, has an agent extract its fields, checks them in plain code, and sends only the exceptions to an accounts team, with every correction recorded against the step that made the mistake.
The pipeline
flowchart LR
W(["webhook: upload"]) --> F[fetch document] --> X["extract (agent)"] --> V[validate]
V -->|no issues| S[/spot check/]
V -->|issues| E[/exception review/]
S & E --> P[post to ERP]
pipeline: invoice-intake
description: Extract, validate and post supplier invoices
vars: { minConfidence: 0.9, autoApproveMaxEur: 5000 }
triggers:
upload: { webhook: { rate: 120/min } }
streams:
docs.extracted: { retention: 400d }
steps:
fetch:
action: ./actions/fetch-document
in: trigger.upload
out: docs.fetched
extract:
action: lab/extract-invoice@1
in: docs.fetched
out: docs.extracted
concurrency: 6
validate:
action: ./actions/validate-invoice
in: docs.extracted
out:
- { when: "size(item.issues) == 0 && item.confidence >= vars.minConfidence", out: docs.clean }
- { when: "true", out: docs.exceptions }
spot-check:
action: review
in: docs.clean
out: docs.approved
config:
inbox: accounts
timeout: 72h
onTimeout: { escalate: accounts-leads }
autoApprove: "item.totalEur <= vars.autoApproveMaxEur"
sampleRate: 0.05
exception-review:
action: review
in: docs.exceptions
out: docs.approved
config:
inbox: accounts
timeout: 48h
onTimeout: { escalate: accounts-leads }
editable: [/supplier, /poNumber, /totalEur, /lines]
reasons: [duplicate, wrongSupplier, missingPo, unreadable]
post:
action: lab/erp-post@1
in: docs.approved
budget: { dayUsd: 40 }
What Millstream does for it
Documents as artifacts
fetchdownloads the file and attaches it to its output as an artifact, stored by content hash in a local directory or S3, never inlined in the item.- The item view previews the PDF beside the extracted fields, the agent's transcript and its cost.
Extraction and validation
extractanswers the action's output schema: supplier, PO number, totals, lines and a confidence.validateis ordinary code: totals add up, the tax matches, the PO exists in the reference data shipped in the package'sassets/.- Each output goes to the first route whose condition holds, so the rule for "clean" lives in the pipeline file.
People on the exceptions
- Exceptions wait in the
accountsinbox; the reviewer corrects the listed fields and approves, or rejects with a reason code. - Clean invoices under 5,000 EUR are approved by rule, and 5% of them still reach a person as review samples, so the rule is measured.
millstream checkwarns that agent output reachespost, an external effect, through a review that auto-approves. The warning is advisory: here the validation code and the samples are the chosen control, and the file says so in plain view.
Failures
- An unreadable scan that fails every retry becomes a dead letter, listed by error in the UI, and returns to its run with
millstream replay --from dlq --run <id>once the action is fixed.
How it learns
A correction to /totalEur is attributed by walking the item's lineage to the nearest step whose output changed that field. That step is extract; validate passed the value through unchanged, so it is not blamed. Three or more such edits on one field become a lessons proposal for extract, with a rule and the corrected examples. When sampled rule approvals start disagreeing with people, the reviewPattern insight raises a warning below 90% agreement.
Figures to watch
| Figure | What it tells you |
|---|---|
| Intervention rate per review step | The share of invoices that still need a person |
| Exceptions by reason code | Whether the problem is the documents or the extraction |
Sampled-rule agreement on spot-check |
Whether the auto-approval rule still holds |
Most edited pointers on exception-review |
Which fields the agent gets wrong |
| Cost per approved item | Extraction cost per posted invoice |