Tech Reads
ERP & Systems9 min read

Integrating AI with Legacy Systems: The Patterns That Actually Work

A client's finance team was running a SOAP API built in 2009 that nobody had touched since. Their AI vendor called it a "read-only data source" and planned to extract from it. The project died six months later when they realised the write path — posting approved invoices back to the ERP — went through the same SOAP API, and nobody had designed for that.

We integrate AI systems with legacy ERP platforms, accounting systems, and operations software that was not designed for API-driven automation. The read side is usually solvable — with enough patience you can extract data from almost anything. The write side is where integrations die. Posting a payment, creating a supplier record, updating inventory — these are the operations that require the system to accept structured input, validate it against business rules, and persist it correctly.

Legacy systems fail at writes for predictable reasons: brittle validation logic, undocumented field requirements, session-based authentication that times out, and no support for the idempotency that modern integration patterns depend on. Here is what we have learned to do instead of fighting these systems directly.

Never write directly to a legacy system in real-time

This is the most important rule we have. Never write directly to a legacy system in real-time from an AI pipeline. Not because it is always technically impossible — sometimes it is — but because a real-time write to a legacy system means your AI pipeline is now coupled to the legacy system's availability, performance, and error behaviour.

Legacy systems go down for maintenance. They return cryptic errors at 3pm on a Friday when the network team made a firewall change. They time out under load. They fail silently on validation errors that should be exceptions. If your AI pipeline writes directly to the legacy system and the legacy system is slow, your pipeline is slow. If it returns an undocumented error, your pipeline fails.

The pattern we use instead: the AI pipeline writes to a shadow table in a database we control. A separate, durable process reads from that table and writes to the legacy system asynchronously, with retry logic and error handling that we own. The AI pipeline is decoupled from the legacy system entirely. We control the retry semantics. We control the error handling. The legacy system can be unavailable for four hours without stopping the pipeline.

Watch out

If your AI integration writes synchronously to a legacy system, you have inherited that system's reliability characteristics. A 97% uptime legacy system introduces 26 hours of annual downtime into your AI pipeline.
26 hrsof annual downtime imported into your AI pipeline when you write synchronously to a legacy system with 97% uptime

Shadow tables and event sourcing at the boundary

A shadow table is a database table in your integration layer that mirrors the data model of the target system, owned by you rather than by the legacy system. When the AI pipeline produces a result — an approved invoice, a new supplier record, a PO line update — it writes to the shadow table first. The shadow table is your source of truth for what the AI decided.

From the shadow table, a separate synchronisation process handles the actual write to the legacy system. It can retry on failure without re-running the AI pipeline. It can batch writes to reduce load on the legacy system. It can pause writes during a maintenance window and resume without losing data. And critically: it records the result of each write attempt, including errors, so you have a complete history of what was attempted and what succeeded.

We extend this with event sourcing at the boundary: instead of updating shadow table rows, we append events — InvoiceApproved, SupplierCreated, POLineUpdated. Each event is immutable. The sync process processes each event once, marks it consumed, and handles the legacy write. On failure, the event stays unconsumed and retries. On success, it is marked consumed and the result is recorded alongside it. This gives you a complete, auditable history of everything the AI decided and everything that was written to the legacy system.

SOAP, SFTP, and batch imports: making each tolerable

SOAP APIs from the mid-2000s are unpleasant to work with. They use XML, WSDL schemas that are often partially wrong, authentication mechanisms that were designed before service accounts, and error responses that return HTTP 200 with a fault buried in the XML body. The shadow table pattern handles most of this — the sync layer translates your clean internal event into whatever XML the SOAP endpoint requires. We keep the SOAP-specific code in one place, isolated from the AI pipeline.

SFTP drops and batch file imports are different. Some legacy systems have no API at all — the only way to push data in is to drop a formatted file in a specific directory at a specific time and wait for a batch process to pick it up. This is still a viable integration pattern, just a slower one. The AI pipeline writes to the shadow table as normal. The sync process formats the data into the required file format (often fixed-width text, occasionally CSV with specific encoding requirements that took us three days to identify on one project) and drops it to the SFTP server.

The hard part of batch integration is confirmation. The file drops. Did the legacy system process it? Did it error? Most legacy batch systems do not return a structured confirmation. They write a log file to a different SFTP directory. Sometimes. The integration pattern here: poll the output directory for a result file after the expected processing window, parse whatever output format the legacy system generates, and map it back to the shadow table events you submitted. Then alert on anything that does not have a confirmed result within 2x the expected processing time.

Screen scraping is the last resort. We have done it — a procurement system from 2004 with no API and no batch export, where the only write path was the browser UI. We use Playwright against a dedicated service account session, with a rate limiter to avoid triggering the session management. It is fragile and we are honest about that. When the system's UI changes, the scraper breaks. The value of the shadow table architecture here is that the scraper is a thin adapter layer — if it breaks, we fix the adapter, not the AI pipeline.

Polling when the legacy system supports neither webhooks nor real-time API

Modern integration assumes the source system can notify you when something changes. Legacy systems do not. You have two options: poll on a schedule, or query a change log table if the system maintains one.

Most legacy ERP systems do maintain some form of change tracking — a modified timestamp on records, a transaction log table, an audit history. If it exists, use it. A query for "records modified since last poll" is dramatically more efficient than a full table scan, and it captures the window of changes cleanly. On one SAP R/3 integration, the change log table we discovered after three weeks of polling the main tables was already doing what we needed — we just had to find it.

If there is no change log: poll on a schedule with a high-water mark. Record the timestamp of the last successful poll. On each run, query for records modified after that timestamp. Use the system's own timestamps, not a clock you control — clock skew between systems causes missed records. Keep the polling interval realistic for the legacy system's load tolerance; one client's system started returning errors when we polled every 30 seconds because the query was expensive and the system was already under load from batch processing.

The legacy system integration patterns that survive are the ones that treat the legacy system as a slow, unreliable external dependency — not as a database you control. Shadow tables give you a fast, reliable system to build on. The legacy system is just a sync target.

Share

Related reading