All postsEngineering
A Posting Protocol for AI in the General Ledger
Matching bank lines gets the attention. Writing entries into the accounts decides whether every one of them can still be explained. A fixed key per entry, a check on the accounting period, one front door, and the rule that automation never corrects its own entries.
The function was short and well meant. An automation posted entries (wrote them into the accounts, the ledger) for a group of eleven companies: bank fees, currency differences, and the monthly recharge billing each subsidiary for shared IT and finance services. When the system judged one of its own entries wrong, say a fee in the wrong company, the function deleted it and reposted it. The ERP, the group's core business system, allowed that for entries not yet matched to the bank in an open month. Nothing stopped it.
Mid-month, the group agreed a new formula for splitting recharges, a routine update. The function read every recharge posted that month as wrong, deleted each and reposted it with the new split, in nine companies. The accounts came out right, but how they got there was gone; group finance spent weeks finding which entries had been rewritten. The function was switched off before anyone could ask what else it could delete.
Finance automation focuses on matching bank lines to open invoices. But the controls sit where entries are written, which needs a protocol, a fixed set of rules, not just a connection. Once most lines match without a person, most entries reach the accounts unseen, and the rules for writing become the control.
Automated errors go through the human correction process
The rule underneath everything: people correct automated mistakes through the process that corrects human ones, a reversal (an entry cancelling the wrong one) or correcting entry, with a reason and an approver. The system never fixes its own entries and never deletes.
Pat Helland told database engineers this in a 2007 essay, "Accountants Don't Use Erasers": a ledger only grows; you fix an entry by adding one that explains it.
Auditors already test that process. An automation that corrects itself has built a second one, undocumented, with nobody separating doer from approver. So its ERP permissions leave out deleting, parking or changing anything it posted, and the rule holds even on the day the code is wrong.
When every recharge must change in nine companies, someone asks why the system can't reverse its own entries. It prepares reversals and new entries, reason attached, and a person posts them as one approved batch per company. Typing becomes a review; the control stays where auditors expect it.
Every entry follows five steps
First, the key. Each entry carries a key built from the business facts, not a random request number. For a bank fee: company, bank account, statement number, line position. For a recharge: sending company, receiving company, service agreement, month. We use only fields the ERP holds, agreed with finance and IT, so the ERP itself can be asked. Retries always give the same key. Stripe's engineers described such keys for payments in 2017. A key made per request stops a repeated request; only a key from the business facts stops the workflow making the same decision twice.
Choosing fields is finance's trade-off. With the statement number in, a corrected statement resent under a new number creates fresh keys for the same fees. Without it, two genuine fees of the same amount on one day block each other, constantly on a busy account. We kept it in: the key stops the workflow repeating itself, the duplicate check below handles the bank repeating itself.
Second, look first: ask the ERP whether this key exists, trusting the ERP over the workflow's memory. That makes timeouts harmless: unsure whether the entry was created, it never guesses or blindly retries; the next attempt starts with the lookup.
Third, the period. If the month is closed or inside the controller's cut-off window, the case goes to a person with a proposed date. Which month an entry lands in is a judgement about the financial statements, not the automation's.
Fourth, the front door: the ERP's standard interfaces, so its validation, numbering, tax logic and permissions apply. Never straight into its tables.
Fifth, read it back and store the ERP's document number against the case; only then is it finished.
Each entry type sits under a control that already exists
Audited finance functions have key controls, each with an owner and an annual test. Auditors accepted automated entries fastest when each entry type went under one already tested, a mapping we build before go-live with the group controller and control owner. Bank fees went under the bank reconciliation (the check that bank and books agree), currency differences under the month-end revaluation review. Recharges, the harder conversation, landed under approval of service agreements and the reconciliation between group companies before consolidation (combining their accounts). Settlement payments stayed under payment release. For each, we record what the automation does and the evidence it leaves.
When nothing fits, the automation is doing something finance never did by hand: a finding for the control owner, not a clever workaround. The workflow posts under its own named account in every company, so auditors sample its entries as they sample people's.
Duplicate payments get a second, independent check
A duplicate bank fee is a nuisance; a duplicate recharge flows into the monthly settlement between companies and becomes cash moving, often across a border; recovering it means a second payment and a talk with tax. Recharge references make poor identifiers: charges return as corrections, quarter-end adjustments, or with the month written differently. So before each settlement run a separate check looks for near-duplicates: same two companies, same service, same or next month, similar amount. For bank lines it compares account, amount, value date and transaction code across statements. Hits go to a person, even if the key says new.
The ERP's own checks answer a different question: a statement import rejects a statement number it has seen, so a corrected statement under a new number gets through. Where the ERP can block rather than warn, we block; nobody reads a warning shown to an automated account.
Each approved settlement batch produces exactly one payment file, recorded before it leaves. If the transfer fails or its status is unclear, the system never resends; a person checks with the bank. That retry could cost real money; checking takes minutes.
The system reconciles its own actions daily
A separate daily process compares what the workflow thinks it wrote with what the ERP and bank hold: bank lines without a case, cases with no entry or two, workflow entries without a case, recharges unmatched in the receiving company, payments not confirmed exactly once by the bank. Mismatches go to finance with evidence.
Stripe's engineers described a structural version in their Ledger post (February 2024): money passing through a holding account, a clearing account, should leave it at zero, so one search for clearing accounts with a balance finds every broken flow. Bank clearing should empty as statement lines post, intercompany clearing as settlements complete, so the job lists items left there too long, by pair of companies. A stranded item is often the first sign of something posted twice, or not at all.
It rarely reports anything; when it does, it is usually a week or two before the month-end close would. The auditor's question is answered too: every action, each shown to have happened once.
Give it no way to rewrite history
Finance teams judge an automation not by how it reads a bank statement but by whether they can explain every entry. One that can only add entries, and whose mistakes are corrected the way people's always have been, stays explainable.