Acumatica · Events

Event-Driven Architecture for ERP — A Field Guide

Event-Driven Architecture for ERP — A Field Guide is the work that turns a collection of business systems into a coherent operation.

John Kihiu12 min read

An ERP is the system of record for the business, which makes it tempting to bolt every integration directly onto it — a synchronous API call from the warehouse system into Acumatica for every pick, a synchronous call out to the tax engine on every invoice. That works until volume grows or one of those downstream systems has a bad day, at which point the ERP's uptime becomes hostage to the least reliable thing it talks to. Event-driven architecture around the ERP means the ERP publishes facts about what happened — an invoice was released, an order shipped — and lets everything downstream react asynchronously, on its own schedule, without holding up the transaction that generated the fact.

Business events as the integration seam

Acumatica's own business events (Business Events screen, SM301070) are the built-in version of this pattern: you configure a trigger — a field change, a generic inquiry condition, an action — and the platform raises a notification that can call a webhook, send an email, or push a message without you writing custom graph code. The value isn't just convenience; it's that the ERP's core transaction (posting the invoice, releasing the order) commits and completes on its own, and the notification fires as a side effect rather than a blocking dependency. If you instead call an external tax or shipping API synchronously from inside a graph extension's Persist override, a slow or down third party now makes the ERP itself slow or down for every user trying to save that screen.

Business events vs. a custom event handler

Reach for a native business event first — it's configuration, survives upgrades, and doesn't need code review. Drop to a custom RowPersisted handler or a queued background task only when the trigger condition or payload shape is something the Business Events UI genuinely can't express.

Async processing without blocking the transaction

The pattern that holds up under load is: the ERP transaction commits first, and the event that describes it is published after, picked up by a separate process. In Acumatica terms, that means using the built-in business event actions (which are already asynchronous — they run through the platform's own processing, not inline with the user's save) or, for anything heavier, writing the fact to an outbox table in the same database transaction as the business change and having a separate poller or Azure Service Bus/SQS relay pick it up and publish it. The outbox pattern matters specifically because it avoids the classic dual-write bug: if you commit the ERP transaction and then call an external message broker as a second, separate step, a crash between those two steps silently drops the event. Writing to an outbox row in the same transaction guarantees the event is durable if and only if the business change itself is durable.

C# · GRAPH EXTENSION PUBLISHING TO AN OUTBOX
public class ARInvoiceEntry_EventExtension : PXGraphExtension
{
    protected virtual void ARInvoice_RowPersisted(PXCache cache,
        PXRowPersistedEventArgs e)
    {
        if (e.TranStatus != PXTranStatus.Completed) return;
        if (e.Operation != PXDBOperation.Update) return;

        var doc = (ARInvoice)e.Row;
        if (doc.Released != true) return;

        // Same transaction as the invoice release — durable together
        var outbox = new UsrEventOutbox
        {
            EventType = "invoice.released",
            RefNbr = doc.RefNbr,
            PayloadJson = PXJson.Serialize(new
            {
                doc.RefNbr,
                doc.DocDate,
                doc.CustomerID,
                doc.CuryDocBal
            }),
            CreatedDateTime = PXTimeZoneInfo.Now
        };
        Base.Caches[typeof(UsrEventOutbox)].Insert(outbox);
    }
}

Idempotency at the ERP boundary

External systems retry, business events can be reprocessed after a hung status, and an outbox poller can crash mid-batch and re-read rows it already sent. Every consumer of an ERP-originated event — and every inbound handler that writes back into the ERP from an external event — needs to treat delivery as at-least-once. On the outbound side, give every published event a stable ID (invoice ref number plus a version, or a GUID stored on the outbox row) so downstream systems can deduplicate. On the inbound side — say, a webhook from a marketplace triggering an Acumatica sales order creation — check for an existing order tagged with the source system's ID before creating a new one, rather than trusting that the webhook fires exactly once.

Watch for reprocessing on integration retries

A common failure in Acumatica-to-external integrations is a timeout on the external side that triggers a retry, while the original request actually succeeded and updated the ERP. Without an idempotency check keyed on an external reference number, retries create duplicate AP bills, duplicate sales orders, or double-posted transactions — expensive to find, worse to unwind.

Ordering guarantees you actually need

ERP events are rarely order-independent — "invoice released" arriving before "invoice created" breaks a downstream consumer that expects to look the invoice up. If you're relaying through a generic message broker, partition or route by document reference number so that all events for a given invoice, order, or shipment are delivered in the sequence they were raised, even if events for unrelated documents interleave arbitrarily. This is also why eventual consistency has to be a conscious design decision, not an accident: a downstream inventory system that reacts to "order shipped" a few seconds after the fact is fine; a downstream system that needs the ERP's number to be authoritative at the instant of the API call is a sign you actually need a synchronous read, not an event.

Monitoring what actually got published

The failure mode unique to ERP event integrations is silent drift — a business event trigger gets deactivated during an upgrade, or a workflow change means the field that used to flip no longer flips, and nobody notices until a downstream system asks where three weeks of orders went. Business Events in Acumatica log their execution history, and that log is worth alerting on: a sudden drop in event volume for a trigger that normally fires daily is a stronger signal than waiting for a downstream complaint. Pair that with a dead-letter path on whatever's consuming the events externally, so failed deliveries are visible and replayable instead of quietly discarded.

ConcernERP-side pattern
Don't block the transactionBusiness events / outbox, not synchronous external calls in Persist
Durable publishOutbox table in the same DB transaction as the business change
Duplicate deliveryIdempotency key on inbound and outbound sides
OrderingPartition/route by document reference number
Drift detectionAlert on business event execution log volume, not just errors

None of this requires abandoning Acumatica's built-in tooling for a bespoke event platform — business events plus a disciplined outbox pattern cover most integration needs. What changes is the default: publish a fact after the transaction commits, make every consumer idempotent, and treat ordering and monitoring as first-class requirements rather than something to add after the first duplicate-order incident.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.