# Invoice Automation Workflow Blueprint — [Company name]

Status: [Draft] · Version: v1.0 · Applies from: [date]

The executable shape of the invoice document lifecycle: what starts it, what each
step consumes and produces, who performs it, how it branches, and what has to be
true before you would let it run without watching. It is the companion to the
Invoice Automation SOP, which describes the same process in prose, and its step
numbers follow that document. The two human gates are shown as `8H` and `9H`.

This blueprint stops at the posted document. Matching to a purchase order,
approval of the spend, the payment run and bank reconciliation belong to the
Accounts Payable Workflow Blueprint.

Placeholders in [brackets] are yours to fill in. This describes a process, not
any particular product's connectors or extraction engine. Educational template,
not financial, accounting or tax advice.

---

## Trigger and termination conditions

**Start conditions**

- `document.arrived.email` — a message with one or more attachments reaches
  [AP mailbox]. One run per attachment, never one per message; a message with
  four PDFs is four documents.
- `document.arrived.portal` — the [daily 09:00] sweep of [supplier portal] finds
  a document not yet registered.
- `document.arrived.scan` — a batch appears in [scan inbox]. One run per file in
  the batch.
- `document.arrived.forward` — a colleague forwards an invoice from their own
  mailbox. Treated as a first-class channel, not an exception.
- `schedule.statement_reconciliation` — [weekly, Monday 07:00], one run per
  supplier statement received.
- `manual.reprocess` — a person reopens a registered document by its identifier,
  with a reason recorded. Never a silent re-run.

**Terminate successfully when**

- the document reaches `posted`, the original file is attached to the posted
  record, and the audit record is complete; or
- the document is classified as something other than an invoice, routed under D3
  and closed with its classification retained; or
- the document reaches `attached_as_duplicate` against an existing record; or
- the document reaches `rejected_to_supplier` with a reason and the outbound
  message retained.

**Terminate unsuccessfully when**

- extraction has failed twice on a document the supplier has already re-sent
  once, at which point a person owns it rather than the process; or
- an exception has been open beyond [3] working days, at which point it
  escalates rather than expires; or
- the coding target is still invalid after a human repair — the process does not
  invent an account to clear a document.

**Never start when**

- the same file hash is already registered and its run is still in flight; or
- the document identifier is already in state `posted`; or
- the channel is in [quarantine] following a bulk mis-delivery, where a person
  triages the batch first.

---

## Ordered steps with data contracts

| # | Step | Actor | Consumes | Produces |
| --- | --- | --- | --- | --- |
| 1 | Receive | Deterministic code | `channel`, `raw_bytes`, `sender`, `received_at` | `document{doc_id, sha256, channel, sender, received_at, bytes_uri}` |
| 2 | Register | Deterministic code | `document` | `state = registered`, immutable arrival record |
| 3 | Fingerprint (byte key) | Deterministic code | `sha256` | `byte_key`, `exact_file_matches[]` |
| 4 | Classify | Model judgment | `page_images[]`, `text_layer?` | `classification{type, confidence, evidence[]}` |
| 5 | Split or keep whole | Deterministic code | `page_images[]`, boundary markers | `child_doc_ids[]` each with `origin_doc_id` and `page_range` |
| 6 | Extract fields | Model judgment | `page_images[]`, `supplier_master`, `field_schema` | `fields{name → {value \| absent, confidence, page, region}}` |
| 7 | Validate | Deterministic code | `fields`, `supplier_master`, `tax_rules`, `D5` | `validation{checks[], failures[], arithmetic_ok, currency_ok, age_ok}` |
| 8 | Route by confidence | Deterministic code | `min(confidence)` over money fields, `validation`, `bands_version` | `route ∈ {auto_post, field_repair, full_review, reject}` |
| 8H | Repair or confirm (A2) | Human | one field or the whole document, with `page`, `region` and the original beside it | `repair{field, before, after, by, at}` |
| 9 | Code | Deterministic code, with a model proposal on new patterns | `line_descriptions[]`, `supplier_coding_history`, `chart_of_accounts` | `coding{account, cost_centre, project?, rule_id \| proposed}` |
| 9H | Confirm new coding (A3) | Human | `coding.proposed`, the document, prior coding for this supplier | `coding_rule{approved_by, at, written_back}` |
| 10 | Post | Deterministic code | `fields`, `coding`, `doc_id` | `posting{ledger_ref, doc_id}`, original attached to the record |
| 11 | Hand over | Deterministic code | `posting` | `payable.registered` event consumed by the payables process |
| 12 | Reconcile to statement | Deterministic code | `statement_lines[]`, `posted_documents[]` | `reconciliation{matched[], statement_only[], ledger_only[], differences[]}` |

Field-level contract notes:

- **Confidence is per field, never per document.** A document-level score hides
  the one field that matters. Step 8 reads the minimum across the money-changing
  fields only: `supplier_identity`, `invoice_number`, `currency`, `net`, `tax`,
  `gross`.
- **Every money field carries its `currency`.** No step compares or sums amounts
  across currencies, and no step in this process converts one.
- **A missing field is `absent`, not `""` or `0`.** An empty string in a total is
  indistinguishable from a zero invoice downstream.
- **Every extracted value keeps `page` and `region`.** This is what lets a figure
  in the ledger be traced back to the part of the document it was read from, and
  what a reviewer is shown at 8H.
- **`classification.type` is a closed set:** `invoice`, `credit_note`,
  `statement`, `delivery_note`, `other`. A model that cannot fit a document into
  the set returns `other` and escalates; it does not invent a type.
- **No step in this process writes to the supplier master.** Not the tax ID, not
  the default currency, and never the bank details.

---

## Step classification (deterministic / model judgment / human approval)

**Deterministic — steps 1, 2, 3, 5, 7, 8, 9, 10, 11, 12.** Hashing, registering,
splitting on boundary markers, arithmetic, threshold application, coding lookup
against an existing rule, posting by identifier, and set-based reconciliation.
Identical inputs must give identical outputs. If one of these needs a model, the
rule underneath it has not been written down yet.

**Model judgment — steps 4 and 6, and the proposal half of step 9.** Deciding
what a document is, reading fields off a layout nobody has seen before, and
proposing coding where the line description matches no known pattern. Two
hundred suppliers means two hundred layouts, which is genuine open-ended
variety. The model reads and proposes. It does not decide whether something
posts — step 8 does that from the confidence and the validation result.

**Human — 8H and 9H, plus A4, A5 and A6 in the SOP.** These exist because
someone has to be answerable for a figure entering the ledger that nothing could
check, for a new coding rule that will apply to every future document from that
supplier, and for a document leaving the building addressed to a supplier.

---

## Branches and retries

| Condition | Branch |
| --- | --- |
| `classification.type ≠ invoice` | D3: route the document (credit note, statement, delivery note, other), close it, no ledger entry — E7 |
| `classification.confidence` below [low threshold] | Human classifies. Two `other` results on the same file end the run |
| One file contains several documents | E2: split into registered children, each carrying `origin_doc_id` and `page_range` |
| One document spans several files | E2: join before extraction; never extract from a fragment |
| Page unreadable or no usable text | E1: no extraction attempted, re-send requested quoting the document identifier. Do not guess figures from a partial read |
| `exact_file_matches` non-empty, or content key already posted | E6: attach the later arrival to the existing record with its own timestamp. Post once |
| `currency ≠ supplier.default_currency` | E3: hold, human confirms against the document and the contract. The process never converts |
| Tax ID absent or malformed where [jurisdiction] requires it | E4: block posting only, request a corrected document. Never amend the supplier's document |
| `sum(line_items) ≠ stated_net`, or `net + tax ≠ gross`, beyond D5 tolerance | E5: full review. Read the printed total, record which was correct |
| Any validation failure, whatever the confidence | Overrides the band and routes to a person. A confident figure that contradicts arithmetic never posts unattended |
| Supplier not on the master file | Hold at step 9. The process does not create a supplier to clear a document (A4) |
| Repair open beyond [1] working day | Remind, then escalate per the ladder below |

**Retries.** Retry only steps that are safe to repeat: 1, 2, 3, 7, 12. Extraction
may be retried at most [2] times, and only where the failure was in producing a
result at all — not to fish for a better number. Where two extractions disagree,
take the lower confidence and route accordingly; never average them. Step 10 is
guarded by the posting key rather than by retry policy, so a repeated attempt
updates rather than duplicates. Never auto-retry an outbound rejection: a second
message to a supplier is a new record with its own reason.

---

## Idempotency

- **Byte key** `sha256(raw_bytes)`, computed at step 1. Catches the same file
  arriving twice — the common forward-and-forward-again case.
- **Content key** `hash(supplier_id, invoice_number, invoice_date, gross,
  currency)`, computed after step 6 and checked before step 10. Catches the same
  invoice arriving as a portal download and as an email attachment, where the
  bytes differ but the invoice does not. The byte key alone will not see this.
- **Posting key** is the internal `doc_id`. Step 10 is an upsert on `doc_id`, so
  a repeated post writes nothing new and creates no second ledger entry.
- **Split children are deterministic:** `child_doc_id = hash(origin_doc_id,
  page_range)`. Re-splitting the same file yields the same identifiers rather
  than a second set of documents.
- **Repairs are append-only events.** Replaying the event log reproduces the same
  field state, and the original extracted value is never overwritten in place.
- **Reconciliation is set-based.** Step 12 is safe to run repeatedly on the same
  statement; matching an already-matched line is a no-op.

---

## Escalation and rollback

**Escalation ladder.** Exception or repair open [1] working day → remind the
assignee. [3] days → [AP Lead]. [5] days → [Financial Controller] and onto the
month-end exception list. Any document whose due date falls inside the ladder
skips it and goes straight to [AP Lead]: a lost early-payment discount or a late
charge is a real cost, and a queue is not a neutral place to leave a document.

**Rollback by stage.**

- Before step 10: fully reversible. Void the document record, keep the arrival
  record and the reason. Nothing has entered the ledger.
- After step 10, before hand-over: reverse the posting with an adjusting entry
  that references `doc_id`. Never delete the original entry — a corrected ledger
  and an edited ledger look identical afterwards, and only one of them is
  defensible.
- After hand-over: the document is now a payable. It is corrected under the
  payables process, and this process records the correction against `doc_id`
  rather than reaching into the payable.
- A rejection already sent to a supplier cannot be unsent. The follow-up is a new
  outbound record linked to the same document.

**When the process stops trusting itself.** Stop the automated path and route to
full review if: the repair rate for one supplier's layout exceeds [threshold]
over [n] consecutive documents; classification returns `other` twice for the
same file; or the unmatched count at step 12 exceeds [n] lines for a supplier in
one statement period. In each case something upstream changed — a new template,
a new channel, a new entity — and a person should find out what before the
process keeps posting.

---

## Permissions and sensitive data

- **Least privilege by step.** Steps 1–3 need read access to the channels and
  write access to the document store only. Steps 4–9 need read access to the
  supplier master, the chart of accounts and coding history, and write access to
  the document record. Only step 10 may write to [accounting system], and only
  to create or update a posting keyed by `doc_id`.
- **No path from this process to master data.** Supplier records, default
  currencies, tax IDs and bank details are maintained by [Vendor Master
  Administrator] outside this process. An automated capture step that can also
  edit a payee record has removed the control it was supposed to feed.
- **Sensitive fields on the document:** bank account and IBAN, tax registration
  numbers, contract pricing, and any personal data on an expense-type invoice.
  Mask these in logs, notifications and anything sent to a model beyond what the
  extraction step needs. Retain them only inside the stored original.
- **Logs record field names, confidences and regions — not full document text.**
  A log line is a copy of the invoice if you let it be.
- **The original file is immutable.** Its hash is recorded at arrival and checked
  before posting. Corrections are new records; nothing edits the source bytes.
- **The audit trail is append-only.** Every repair, classification, exception and
  approval records who, when, and which version of the bands and rules was in
  force.
- **Retention.** The stored document plus its extracted fields must still show
  the payee, the amount, the date incurred and a description years later; see the
  IRS guidance on [what kind of records to keep](https://www.irs.gov/businesses/small-businesses-self-employed/what-kind-of-records-should-i-keep),
  on [automated records](https://www.irs.gov/businesses/automated-records), and
  on [recordkeeping periods](https://www.irs.gov/taxtopics/tc305). Confirm the
  period that applies to you with your accountant. The mailbox the invoice
  arrived in is not an archive.

---

## Test cases and acceptance criteria

Run each case on your own historical documents before the process handles live
volume unattended. Every case states what must be true for it to pass.

| # | Case | Passes when |
| --- | --- | --- |
| T1 | Clean invoice, all money fields above the high band, every check passed | Posts unattended; bands version and per-field confidences recorded |
| T2 | The same PDF forwarded twice | Byte key catches it at step 3; second arrival attached, not posted |
| T3 | Same invoice downloaded from the portal and emailed by the supplier | Content key catches it before step 10 even though the bytes differ |
| T4 | Five invoices in one scanned file | Five child documents, each with `origin_doc_id` and `page_range`; ids stable on a re-split |
| T5 | One invoice spanning three pages | One document, one posting — not three |
| T6 | Invoice in a currency other than the supplier default | E3 raised; no conversion performed anywhere in the run |
| T7 | Line items do not sum to the printed total | E5 raised; full review; the resolution records which figure was correct |
| T8 | Tax ID missing where [jurisdiction] requires it | E4 raised; posting blocked; corrected document requested; the supplier's document is not amended |
| T9 | Photograph of a crumpled receipt | E1 raised; no extraction attempted; re-send requested quoting the identifier |
| T10 | Delivery note sent to the AP mailbox | Classified, routed under D3, closed; no ledger entry created |
| T11 | Supplier absent from the master file | Held at step 9; the process creates no supplier |
| T12 | Line description matching no known pattern for this supplier | Coding proposed, not applied; A3 required; the approved rule is written back |
| T13 | High confidence on every field, but `net + tax ≠ gross` | Validation overrides the band; the document does not post unattended |
| T14 | Supplier statement re-imported | Step 12 is a no-op on already-matched lines |

**Acceptance criteria before this runs unattended**

- T1–T14 pass on documents you have actually received, not only on invented
  ones. Synthetic invoices are uniformly legible in a way real post is not.
- Every confidence band has fired at least once during testing, including the
  reject band.
- A sample of auto-posted documents has been re-checked by a person against the
  originals, and the sample is retained.
- Every test document produces a complete audit record: original file, extracted
  values with regions, repairs, approvals and the rules version in force.
- A named finance owner has read this blueprint and the Invoice Automation SOP
  together and agrees they describe the same process.

## Revision log

- v1.0 — 2026-07-24 — initial draft — [Your name].
