All postsArchitecture
Data Contracts for ERP Exports
A language model will read a changed export without complaint, which is exactly why it must never sit at a data boundary. The contract we enforce on every batch: shape, meaning, completeness, timing and ownership, with quarantine instead of repair and a contract that later becomes the migration spec.
The match rate of a finance workflow fell sharply one Monday morning, and nothing had failed. No error, no alert, no stopped job. A weekend update to the ERP had changed the date format in one export from day-first to month-first. The batch covered the first nine days of March, so every date in it still parsed. It parsed wrongly. Invoices were being matched against receipts from the wrong month, calmly and at full speed.
Had the batch run past the twelfth, every later row would have failed to parse and someone would have looked within minutes. The silent version needed a calendar coincidence. Across enough exports and enough upgrades, coincidences like that stop being rare.
The fix took an hour. Finding it took most of a day, because nothing in the design assumed an export could change its meaning without changing its shape.
Every AI workflow inside an operation depends on data nobody promised to keep stable: exports scheduled years ago, fields reused for new purposes, codes that differ by country. The ERP cannot sign a contract. We write one anyway and enforce it on every batch.
Consumer-driven contracts, as Ian Robinson described them on Martin Fowler's site in 2006, let the consumer of a service state what it relies on and have the provider test against it. A legacy ERP will never run anyone's tests. So the consumer writes the contract and enforces it alone, and the design question becomes which component is strict enough to do the enforcing.
Models are too forgiving to enforce it
Language models are good at reading messy input. A renamed column, a new date format, an extra field: the model copes. Teams take this as a feature and let the model absorb format changes.
At a data boundary it is a liability. A traditional parser that meets a changed export fails loudly, and someone looks. A model that meets the same change produces a plausible answer and moves on, and the error surfaces weeks later in a reconciliation, far from its cause. The boundary should be the least forgiving component in the system. The model receives only data that has already passed it.
A model may read a document's content. It does not get to decide what the columns of a system export mean.
Engineers will recognise the principle being broken. Jon Postel's rule, written into the TCP specification in 1980, says to be liberal in what you accept. It made early networks work, and it is still good advice for reading free text. At a boundary between systems it has a known cost, which Martin Thomson and David Schinazi set out in RFC 9413 (2023): a tolerant receiver hides the sender's mistakes, the mistakes become the de facto format, and nobody can fix them later. A language model is the most tolerant receiver ever put into production. Put it at the boundary and every change the ERP makes becomes a new format that only the model understands, until the day it misunderstands one.
What the contract covers
Most teams write down the first of these five and forget the rest.
Shape. Fields, types, formats, mandatory values. Dates in one declared format. Amounts with a declared decimal separator and sign convention.
Meaning. What a field represents, per source. Whether "posting date" is the document date or the entry date in this market. Whether credit notes arrive as negative amounts or as positive amounts with a document type. Two exports can share a shape and differ in meaning, which is why multi-market data breaks here.
Completeness. How many records should arrive and what they should add up to. The totals must come from somewhere other than the export itself: a count and sum from the ERP's own open-items report, or the control totals the bank puts in its statement. An export that checks itself will agree with itself. A batch that is perfect in shape and missing a hundred rows is the most dangerous kind, and we keep the check even for sources that "never lose rows". Every source loses rows eventually, and it happens on a day nobody is looking.
Timing. When the batch lands, which period it covers, and what happens to records posted after the cut-off. A workflow that reads an export before the warehouse posts its receipts creates exceptions that are really just lateness.
Ownership. Who on the client's side approves a change to the export, and how we hear about it. Without this, the contract is a document nobody reads until after the incident.
In practice the contract for one export is a page: the fields with type and meaning, the sources of the control totals, the delivery window, the sentinel records, the owner, and a version number.
Hard checks stop the batch, soft checks raise a hand
Shape and completeness checks are hard: a batch that fails them does not proceed. Meaning is harder to check mechanically, so we add two kinds of softer evidence.
The first is the shape of the data rather than of the file. Each batch is compared with the last few weeks from the same source: the share of credit notes, the spread of amounts, the number of distinct suppliers, the distribution of dates across the month. A day-first to month-first switch has a fingerprint. Read day-first, every March date in that batch landed on the third of some month, because the day field now held the month number. A check on the distribution of days would have flagged the first batch before a single match was made. These checks do not stop a batch on their own, because real business changes move distributions too. They put a named person in front of it before it is processed.
The second is sentinels: a handful of known records whose correct reading we know, checked on every run. If a known invoice from a known supplier suddenly reads as a different date or amount, the meaning has changed even though the shape has not. Sentinels are the closest thing a consumer has to the provider-side tests Robinson described.
Failed batches are quarantined whole
The contract is code. Every incoming batch is checked against it before any business logic runs. A batch that fails is not partly processed and not repaired by guesswork, even when the fix looks obvious. Obvious fixes applied silently are how one wrong assumption becomes a month of wrong postings. The batch is quarantined with a reason, and a named person is told.
Quarantine feels slow to people who want throughput. A rejected batch costs an hour of someone's attention. A misread batch costs days of investigation and, in finance, a correction trail the auditors will ask about.
Quarantine also moves the cost of a change to the people who can prevent it. When the consumer quietly absorbs every change, nobody upstream hears that an upgrade affected a workflow downstream, and the next upgrade can do the same. When a batch stops, the question lands where the change was made. We report quarantines per source each month to the owner of the export. In the first months the number is uncomfortable. Then it falls, because changes start arriving with notice.
Whole-batch quarantine has one real cost: a single bad row holds up hundreds of good ones. We accept that for anything that posts to the ledger. For read-only uses, such as a dashboard or a triage queue, a contract may allow row-level rejection, as long as rejected rows are counted and the count is checked against the control totals like any other.
Every source gets its own contract
When we connected four European markets for Bredent Medical, each on Dynamics 365 or a legacy system, there turned out to be no such thing as one ERP contract. Each market had configured its system differently, used its own codes and exported on its own schedule. One integration layer sat in front of all four, with a contract per source and a mapping per market into one shared model. The contracts are what kept that layer trustworthy as each market kept changing its own configuration. That matters most when a business is adding markets: every new market arrives as a new source, and it should arrive with its contract.
Bank data needs the same discipline. MT940 statements and ISO 20022 camt.053 messages carry references, value dates and booking dates that decide matching quality long before a model is involved, and banks change field usage as they migrate between formats. A contract per bank and account type, with sentinels, catches that before the reconciliation does.
Changes run in parallel, not overnight
Exports will change, and sometimes the change is right. The contract is versioned, and a planned change follows the parallel change pattern Danilo Sato described on Martin Fowler's site in 2014: the source publishes the new version beside the old one, the workflow reads both and compares them for a period that includes a month-end, and only then is the old version retired. A change that cannot run in parallel goes live with the quarantine switched to its strictest setting for the first batches.
Contracts are tested with broken files
Each contract ships with sample files: good ones, and deliberately broken ones with a renamed field, a changed date format, missing rows or a wrong total. The boundary must accept the first and reject every one of the second, and this runs in the test suite on every change. When the ERP partner schedules an upgrade, the client's team can pass a sample export from the test system through the boundary and know within minutes whether anything breaks.
We also agree one line with the client's IT and the ERP partner: a change to an export that feeds a workflow goes through the same change process as a change to the workflow. That line has prevented more incidents than any validation code we have written.
The contract outlives the ERP
There is a return on this work that nobody budgets for. Most of the companies we work with will replace or upgrade their ERP within a few years. When they do, the hardest part of the project is discovering what every downstream process actually relies on: which fields, in which meaning, on which schedule. A company with data contracts already has that list, written down, tested and versioned. The contract written to protect one AI workflow becomes the specification for the migration, and the broken-file tests become the acceptance tests for the new system's exports.
Loud failures are the goal
Most of what makes an AI workflow reliable in production happens before the model sees anything. A contract does not stop exports from changing. It makes the change loud, and loud failures get fixed in an hour. Silent ones get found by the auditor.