All postsEngineering
A Posting Protocol for AI in the General Ledger
Matching bank lines gets the attention. Writing to the ledger decides whether every entry can still be explained. Business keys, period checks, one front door, and the rule that automation never corrects its own entries.
The function was short and well intended. It lived in an automation that posted the journals coming out of bank statements and intercompany recharges for a group of eleven entities: bank fees, FX differences on receipts in foreign currency, the monthly recharge of shared IT and finance services to each subsidiary. Whenever the system decided one of its own postings was wrong (a fee in the wrong entity, say), the function deleted the entry and posted a corrected one. The ERP allowed that for unreconciled journals in an open period. Nothing stopped it.
Then, partway through a month, the group agreed a new allocation key for the recharges, a routine update after headcount moved between two subsidiaries. The function read every recharge already posted that month as wrong. It deleted each one and posted it again with the new split, in nine entities. The ledgers came out right. How they got there was gone, and it took weeks of patient work in group finance to establish which entries had been rewritten. The function was switched off before anyone could ask what else it was able to delete.
Most of the attention in finance automation goes to matching: which bank line belongs to which open item. The controls sit at the point where something is written to the ledger, and that needs a protocol, which is more than an integration. As matching improves this gets more important, oddly enough. Once most statement lines are matched without a person, most of the postings they produce reach the ledger with nobody looking at them first. At that point the rules for writing are the control.
Automated errors go through the human correction process
The rule underneath everything else: when automation makes a mistake in the ledger, the people and the process that correct human mistakes correct it. A reversal or a correcting entry, raised by the finance team, with a reason and an approver. The system does not fix its own entries, and it never deletes.
Pat Helland put this to database engineers in a 2007 essay called "Accountants Don't Use Erasers". A ledger only grows; you fix an entry by adding another one that explains it. The function in that group did the reverse and kept no trace of the first entry. Engineers building event logs took the idea of an append-only record from bookkeeping in the first place. Automation that writes into a real ledger has to give it back.
We hold to this for a practical reason. The correction process is the bit of finance the auditors already test and the controllers already trust. An automation that corrects itself has quietly built a second correction process, with no documentation and no segregation of duties. So its rights in the ERP leave out deleting, parking or changing anything it has posted, which means the rule still holds on the day the code is wrong.
Finance teams feel the cost of this first. A new allocation key can mean every recharge of the month has to change in nine entities, and reversing those by hand is dull work. Someone always asks why the system can't just reverse its own postings. What we do instead is let it prepare the reversals and the new entries, with the reason and the affected journals attached, and a person posts them as one approved batch per entity. It becomes a review rather than a morning of typing, and the control stays where the auditors expect to find it.
Every posting follows five steps
First, the key. Each posting carries a key built from the business identity of what is being posted, not a request ID. For a bank fee we use the entity, the bank account, the statement number and the line's position on the statement. For a recharge it is the sending entity, the receiving entity, the service agreement and the period. We build it only from fields the ERP already holds, agreed with finance and IT, so the ERP itself can be asked about it. In SAP that usually means the header reference; in Dynamics 365 Business Central, the external document number. However often the workflow restarts or retries, the same statement line gives the same key. Stripe's engineering team wrote about idempotency keys for payment APIs back in 2017, and the idea carries over, with one twist inside an ERP. A key generated per request protects you from a duplicate request. Only a business key protects you from the workflow making the same decision twice.
Picking the fields is a trade-off, and we would rather finance made it with open eyes. Put the statement number in, and a bank that sends a corrected statement under a new number creates fresh keys for the same fees, so the key on its own lets both through. Leave it out, and two genuine fees for the same amount on the same day block each other, which on a busy account happens all the time. We kept the statement number in. The corrected statement is caught by the near-duplicate check further down. In other words the key stops the workflow repeating itself, and the second check deals with the bank repeating itself.
Second, look before writing. The workflow asks the ERP whether a journal with this key already exists, and believes the ERP over its own memory. This is what makes timeouts harmless. When a posting call times out, the workflow has no way of knowing whether the journal was created, so it doesn't guess and doesn't blindly retry. The next attempt starts with the lookup and carries on from whatever it finds.
Third, the period. If the target period is closed, or falls inside a cut-off window the controller has set, nothing is posted; the case goes to a person with a proposed date. Which period an entry lands in is a judgement about the financial statements, and that judgement isn't the automation's.
Fourth, the front door. Postings go through the ERP's standard interfaces so that its own validation, numbering, tax logic and authorisations apply. Never straight into tables.
Fifth, read it back. After posting, the workflow reads the journal back and stores the ERP's document number against the case. Until then the step isn't finished.
Each posting type inherits a control that already exists
Any audited finance function already has a controls framework, with key controls that each have an owner and an annual test. We found the quickest route to auditors accepting automated postings was to avoid inventing controls for them at all, and to put each posting type inside one that is already being tested.
So before go-live we sit down with the group controller and the internal control owner and build that mapping. In this group the bank fees went under the bank reconciliation control they already ran. FX differences sat with the month-end revaluation review. Recharges were the harder conversation, and they ended up under two existing controls: the approval of the service agreements, and the intercompany reconciliation that runs before consolidation. The netting payments that settle the recharges stayed under payment release, segregation of duties unchanged. For each one we write down what the automation does inside the control and what evidence it leaves behind for the test.
Sometimes nothing fits. We treat that as a finding. It usually means the automation is doing something finance never did by hand, and that needs a decision from the control owner, not a clever workaround. The workflow also posts under its own named service account in every entity, so auditors can sample its entries with the procedures they already use for people's.
Duplicate payments get a second, independent defence
A bank fee posted twice is a nuisance. A recharge posted twice goes into the monthly netting and turns into cash moving between two entities, often across a border, and getting it back means a second payment and a conversation with the tax team.
Recharge references make poor identifiers. The same charge comes back as a correction, or as a quarter-end true-up, or with the period written some new way. So before each netting run a separate check looks for near-duplicates: the same pair of entities and the same service, in the same or an adjacent period, for a similar amount. On the bank side it compares account, amount, value date and bank transaction code across statements. Anything it finds goes to a person, even when the business key says the posting is new.
We don't rely on the ERP's own checks alone. They help, but they were built to answer a slightly different question. A statement import typically rejects a statement number it has seen before, and that is precisely why a corrected statement under a new number gets through. Where the ERP lets a check block a posting rather than warn, we set it to block. A warning shown to a service account is read by nobody.
Payment files get one more rule. Each approved netting batch produces exactly one file, and its identifier is recorded before it leaves. If the transfer fails or its status is unclear, the system does not resend. A person checks with the bank and decides. Of all the retries in the system, that is the one that can cost real money, and checking takes a few minutes.
The system reconciles its own actions daily
Once a day a separate process compares what the workflow thinks it wrote with what the ERP and the bank actually hold. It looks for statement lines with no case, cases with no journal or with two, journals carrying the workflow's reference that belong to no case, and recharges posted in the sending entity with no counterpart in the receiving one. It also checks that each payment in a netting file shows up once in the bank's confirmation. Whatever doesn't line up goes to the finance team as an exception, with the evidence attached.
Stripe's engineering team described a structural version of this check in its post on Ledger (February 2024). When a flow of money completes, the clearing accounts it passed through should be back at zero, so a single query for clearing accounts that still carry a balance finds every broken flow, whatever broke it. A group's ledgers have those accounts already. Bank clearing should drain as statement lines post, and intercompany clearing should drain as the netting settles. So the daily job also lists open items there older than the agreed window, grouped by entity pair. An item stranded in one of those accounts is often the first sign that something was posted twice, or not at all.
Most days the job reports nothing. On the days it does, it has usually found the problem a week or two before the close would have. And when the auditor asks about automated postings, the answer is already on file: every action the system took, and evidence that each one happened once.
Give it no way to rewrite history
Finance teams don't judge an automation by how well it reads a bank statement. They judge it by whether they can explain every entry in the ledger, and an automation that can only add entries, and whose mistakes get corrected the way people's always have, stays explainable.