# Accounts Payable Workflow Blueprint — [Company name]

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

The executable shape of the payables cycle: 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
Accounts Payable SOP, which explains the same process in prose. This blueprint
describes a process, not any particular product's connectors.

Placeholders in [brackets] are yours to fill in. Educational template, not
financial or legal advice.

---

## Trigger and termination conditions

**Start conditions**

- `payable.registered` — an invoice reaches state `registered` with vendor,
  gross amount, currency, invoice date and, for PO-backed categories, a PO
  reference. One run per payable.
- `schedule.payment_run` — [weekly, Wednesday 14:00 local time]. One run per
  cycle, consuming every payable in state `approved`.
- `schedule.bank_import` — [daily, 08:00]. One run per statement file.
- `manual.off_cycle_run` — opened by [Financial Controller] with a reason
  recorded under approval A5.

**Terminate successfully when**

- the payable reaches `reconciled` and its audit record is complete; or
- the payable reaches `cancelled` with a recorded reason and a retained record;
  or
- the payment run reaches `released` and every line in it has a payment
  reference written back.

**Terminate unsuccessfully when**

- a hard gate is declined by its approver (record the decline and the reason,
  never retry silently); or
- an exception has been open beyond [5] working days without a decision, at
  which point it escalates rather than expires; or
- the available balance check fails at step 10, which stops the whole run rather
  than paying part of it.

**Never start when**

- the vendor has an open bank-detail change request (E5); or
- the payable is already in a released run; or
- the same payment run identifier is already in flight.

---

## Ordered steps with data contracts

| # | Step | Actor | Consumes | Produces |
| --- | --- | --- | --- | --- |
| 1 | Assemble payable context | Deterministic code | `payable_id`, `vendor_id`, `po_ref?` | `context{vendor, terms, po, receipt, contract?}` |
| 2 | Check vendor is payable | Deterministic code | `context.vendor` | `vendor_status{active, on_hold, bank_verified_at, open_change_request}` |
| 3 | Three-way match | Deterministic code | `invoice_lines[]`, `po_lines[]`, `receipt_lines[]`, `tolerance_version` | `match_result{status, variance_abs, variance_pct, per_line[]}` |
| 4 | Duplicate check | Deterministic code | `vendor_id`, `invoice_number`, `gross_amount`, `invoice_date` | `duplicate_candidates[]` with similarity reason |
| 5 | Classify non-match | Model judgment | `match_result`, `invoice_text`, `po`, `receipt`, `vendor_correspondence?` | `exception{class ∈ E1..E8, evidence[], proposed_handling}` |
| 6 | Code the payable | Deterministic code | `context`, `tax_rules`, `cost_centre_map` | `coding{cost_centre, gl_account, tax_code, net, tax, gross}` |
| 7 | Select approver | Deterministic code | `gross_amount`, `cost_centre`, `approval_matrix_version` | `approval_request{approver, band, rule_id}` |
| 8 | Approve the spend (A1) | Human approval | `approval_request`, full document set | `approval{decision, approver_name, timestamp, comment?}` |
| 9 | Schedule into a run | Deterministic code | `approval`, `terms`, `D3` | `payment_schedule{run_date, run_id}` |
| 10 | Assemble payment proposal | Deterministic code | `payables[] where state=approved`, `credit_notes[]`, `balance` | `run_proposal{lines[], totals_by_currency, new_or_changed_payees[]}` |
| 11 | Release the run (A2) | Human approval | `run_proposal` | `release{decision, releaser_name, timestamp}` |
| 12 | Remit and write back | Deterministic code | `release`, `run_proposal` | `payment_reference` per payable, `remittance_sent[]` |
| 13 | Reconcile to bank | Deterministic code | `statement_lines[]`, `released_runs[]` | `reconciliation{matched[], unmatched[]}` |
| 14 | File and close | Deterministic code | everything above | `audit_record`, `payable.state = reconciled` |

Field-level contract notes:

- `gross_amount` carries an explicit `currency`; no step may compare amounts
  across currencies without a recorded rate and rate source.
- `match_result.status ∈ {matched, within_tolerance, variance, no_po,
  no_receipt}`. `within_tolerance` always carries the computed variance and the
  `tolerance_version` that judged it.
- `exception.class` is a closed set. A model that cannot fit a case into
  E1..E8 must return `class = unclassified` and escalate, not invent a class.
- Every human decision carries a named person, never a role mailbox or a shared
  login.

---

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

**Deterministic — steps 1, 2, 3, 4, 6, 7, 9, 10, 12, 13, 14.** Assembly,
comparison, arithmetic, threshold application, routing lookup and write-back.
These must give an identical result for identical inputs, and they are the
cheapest and most testable part of the process. If a step here needs a model,
the rule underneath it has not been written down yet.

**Model judgment — step 5 only.** Reading an invoice, a PO, a receipt and a
message thread together and naming which exception class this is, with the
evidence for it. This is the one place in the payables cycle where the variety
is genuinely open-ended. The model names and evidences the exception; it does
not decide the money outcome, change a vendor record, or release anything.

**Human approval — steps 8 and 11, plus A3 whenever E5 fires.** These are
permanent gates, not training wheels. They exist because someone must be
answerable for the commitment and for the money leaving, and because
[authorising, processing and recording, and reviewing must sit with different
people](https://www.gao.gov/products/gao-25-107721). Steps 8 and 11 must be
performed by different named people, and neither may be the person who
registered the payable.

---

## Branches and retries

| Condition | Branch |
| --- | --- |
| `duplicate_candidates` non-empty | E1: hold both, human confirms against vendor statement, cancel the later record with a reason |
| `match_result.status = variance` | E2: return to requester with the three documents; the requester, not AP, resolves it |
| `match_result.status = no_po` | E3: hold, notify requester and category owner, require a documented retrospective PO |
| Tax code unmapped for the vendor jurisdiction | E4: hold the posting only, route to [Tax / Financial Controller], keep the rest of the payable moving |
| `vendor_status.open_change_request` or bank details differ | E5: block payment to that vendor, out-of-band verification, A3 with a second person |
| Vendor not on master | E6: hold, route to vendor review; never create a vendor to clear an invoice |
| Received quantity < invoiced quantity | E7: apply D4 — pay the received portion or hold the whole payable |
| Credit note present | E8: net within the same vendor account only, before the proposal is assembled |
| Approver has not responded within [3] working days | Remind at [24h] and [48h], escalate to the deputy, then to [Financial Controller] |

**Retries.** Retry only steps that are safe to repeat: 1, 3, 4, 6, 13. Use
[3] attempts with backoff for transient read failures. Never auto-retry steps
11 or 12 — a failed release is investigated by a person, because a blind retry
is how a run gets paid twice. A declined approval is a terminal outcome, not a
retryable error.

---

## Idempotency

- **Idempotency key per payable:** `hash(vendor_id, invoice_number,
  gross_amount, currency, invoice_date)`. Computed at step 1, checked at step 4,
  and stored with the payable so a re-registration is recognised as a repeat.
- **Idempotency key per run:** `run_id = hash(entity, run_date, sequence)`.
  Step 10 refuses to assemble a proposal for a `run_id` already in
  `released`.
- **Write-back safety:** step 12 writes the payment reference only where it is
  currently null, so a repeated write-back cannot overwrite a real reference.
- **Approvals are single-use:** an approval token is bound to
  `(payable_id, gross_amount, vendor_id)`. If any of those change after
  approval, the approval is void and the payable returns to step 7. This is what
  stops an approved payable being edited into a different payment.
- **Reconciliation is set-based:** step 13 is safe to run repeatedly on the same
  statement file; matching an already-matched line is a no-op.

---

## Escalation and rollback

**Escalation ladder.** Exception open [2] working days → remind the assignee.
[3] days → escalate to the deputy. [5] days → escalate to [Financial
Controller] and put the payable on the monthly exception review. Anything
touching E5 skips the ladder and goes straight to [Financial Controller].

**Rollback by stage.**

- Before step 8: safe to reverse entirely. Void the payable, keep the record.
- Between steps 8 and 11: void the approval, return to step 7, and notify the
  approver that their approval was withdrawn and why.
- After step 11: the payment cannot be rolled back by the process. It is
  recovered by a person contacting the bank and the vendor, and the process
  records a `payment_incident` linked to the payable. Any blueprint that claims
  to undo a released payment is describing something the bank will not do.
- After step 13: a wrong reconciliation is corrected by reversing the match, not
  by editing the statement line.

**When the process stops trusting itself.** If step 5 returns `unclassified`
twice for the same payable, or if the unmatched count at step 13 exceeds
[5] lines in one run, stop the automated path and hand the queue back to a
person until someone has decided what changed.

---

## Permissions and sensitive data

- **Least privilege by step.** Steps 1–7, 9 and 10 need read access to invoices,
  POs, receipts and vendor records, plus write access to the payable only. No
  step in the automated path may write to the vendor master. Bank-detail changes
  are performed by [Vendor Master Administrator] under A3, outside this process.
- **Payment initiation** is separated from payment release. The process may
  assemble a proposal; only a named human at step 11 may release it.
- **Sensitive fields:** vendor bank account and IBAN, tax registration numbers,
  contract pricing. Mask these in logs, notifications and any model prompt.
  Step 5 receives document text with bank details redacted — the classifier does
  not need them, and anything a model sees ends up somewhere it was not planned
  to be.
- **The audit trail is append-only.** Corrections are new entries with a reason,
  never edits over the top of the original.
- **Retention** follows the entity's records policy; 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)
  and on [automated records](https://www.irs.gov/businesses/automated-records),
  and confirm periods with your accountant.

---

## Test cases and acceptance criteria

Run each case on test data before the process handles a real payment run. Every
case states what must be true for it to pass.

| # | Case | Passes when |
| --- | --- | --- |
| T1 | Clean PO-backed payable inside tolerance | Reaches step 8 with no exception; variance and tolerance version recorded |
| T2 | Same invoice registered twice | Second registration blocked at step 4; both records retained; no second payment |
| T3 | Price variance above D1 | E2 raised, routed to the requester, payable held out of the run |
| T4 | Invoice with no PO in a PO-backed category | E3 raised; no retrospective PO is created by the process itself |
| T5 | Vendor absent from the master | E6 raised; process does not create the vendor |
| T6 | Bank-detail change requested by email | E5 raised; vendor blocked; A3 required; verification recorded against a number already on file |
| T7 | Partial delivery, [8%] short | D4 applied; only the received portion payable; expected receipt date recorded |
| T8 | Credit note against an earlier invoice | Netted within the same vendor account only; run total correct |
| T9 | Unmapped tax code | E4 raised on the posting only; the rest of the payable continues |
| T10 | Payable edited after A1 approval | Approval voided; payable returns to step 7 for re-approval |
| T11 | Payment run released twice | Second release rejected by the `run_id` check; no duplicate payment |
| T12 | Bank statement re-imported | Step 13 is a no-op on already-matched lines |
| T13 | Approver on leave | Reminder, then deputy, then controller — the run is not silently delayed |
| T14 | Model returns `unclassified` | Escalates to a human; no default handling applied |

**Acceptance criteria before this runs unattended**

- T1–T14 pass on test data, and the results are stored where a reviewer can
  re-read them.
- Steps 8 and 11 are performed by different named people in every test run.
- Every test payable produces a complete audit record with no missing approver,
  timestamp or reference.
- The exception classes have each fired at least once on real historical data,
  not only on invented cases.
- A named finance owner has read this blueprint and the SOP together and agrees
  they describe the same process.

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