brdy.ai Book a call
Insight 01 · ERP integration architecture

Read widely, write narrowly.

An operations agent that can read every system and write to any of them does not have an integration. It has a race between systems to be the last one written. Here is the rule we run instead, what it buys, and what it costs.

Published · Brdy AI

The failure this rule exists to prevent

In a live commerce stack the same product exists in several places at once. It is a record in the ERP. It is a listing on more than one storefront. It is a row in the shipping platform and a quantity on a shelf. Each of those systems has an API, and every one of those APIs will happily accept a write.

So the tempting design is the direct one: the agent works out that a price should change, and it changes the price on the storefront. One call, fast, visibly correct for about a day. Then the ERP exports its own version of that price and the change is gone. Or it does not export, and the two numbers disagree quietly until somebody reconciles an invoice by hand. The system now holds two truths and has no way to say which is real, because both writes succeeded.

The rule

“Butler reads widely and writes narrowly, always into the system of record, always through a human. It never writes a storefront directly — every price and every listing goes into Odoo, and Odoo's existing connector exports it.”

Butler architecture doctrine

Reading is cheap and nearly free of consequences, so Butler reads from everywhere it is useful to read: the ERP, the storefronts, the shipping platform, the mailbox, the messaging workspace. Writing is where the consequences live, so writing is restricted to one destination per fact. A price is written to Odoo. A listing is written to Odoo. The storefronts sit downstream of that write, reached by the ERP's own connector — code we did not write, and therefore do not have to re-verify every time we change something.

What the rule buys

/01

One place to look

When a price is wrong, the question is what the ERP record says and who changed it. There is no second write path to rule out first.

/02

Rollback is an edit

Undoing a bad change means correcting one record and letting it export again, not reconciling several storefronts against each other under time pressure.

/03

A short audit surface

Reviewing what an agent can do to a business means reviewing its write paths. Keeping that list small is what lets the review actually finish.

And what it costs

Routing every write through the system of record means inheriting that system's export path, including its latency and its limits. If the connector between the ERP and a storefront cannot carry a particular field, the agent cannot set that field either — and nothing appears on the storefront until the export runs.

That is a real constraint and it is worth naming, because the moment it bites there is an obvious escape hatch: open a second, direct write path, just for this one field, just this once. That hatch is how systems end up with two truths. The supportable answers are to extend the export path or to accept that the field is not automated. Adding a write is not one of them.

Writing narrowly at the edge: the merchant API

The same rule has to hold when the caller is somebody else's software. Butler exposes a merchant API where partners pull inventory and push sales orders. Those pushes are not a queue that a person re-keys later: they create and confirm real sale.order records in Odoo and deduct real inventory.

Two properties are what make that safe enough to hand to a third party.

Idempotency on the order reference. A partner's system retries. It has to: from the caller's side a network timeout and a silent rejection look identical, and the only safe behaviour available to a caller that does not know which one happened is to send the request again. If the receiving side treats that second submission as a new order, the caller's safe behaviour produces a duplicate order and a duplicate inventory deduction. Keying on the caller-supplied order reference means a retry resolves to the same order instead of a second one. Any integration where the caller may retry and the callee creates financial records needs a key the caller controls.

Fail closed. When the write into the ERP cannot be completed, the call fails. It does not half-succeed, and it does not fall back to recording the order locally in the hope of replaying it later. A local record the system of record never sees is exactly the second truth this whole design exists to prevent — and it is worse than an error, because an error gets retried and a shadow record gets forgotten.

The unglamorous part: schema

Butler's own database is 120 Postgres tables under 138 migrations, written as raw SQL with no ORM. Those counts come from the project's internal capability document for the PPC International deployment. Every migration is written to be idempotent, because the same migration gets applied to a development stack, a preview stack and production at different times and in different states — and a migration that only works against a clean database is a migration that works exactly once.

Raw SQL is a deliberate trade. It is more to write, and in exchange the schema stays legible to anyone who can read SQL rather than only to someone who knows one ORM's conventions. When an integration misbehaves, the question is almost always what is actually in the table right now, and SQL answers that question directly.

If you are starting this

Enumerate the writes before building anything. List every system that will accept a write from your agent, pick exactly one destination per fact, and make every other system a reader or an exporter. The list of things an agent may read can be as long as you like. The list of things it may change is the design.

Butler integrates with Odoo ERP and Shopify, among other systems. Integration is not partnership and not certification: neither vendor has endorsed, reviewed or been involved in this work.