Business Events are Acumatica's built-in mechanism for reacting to data changes without writing a customization project for every notification or handoff. The feature lives under System Automation, and once you've configured a handful of them the shape becomes familiar: a source, a trigger condition, and one or more subscribers.
The three parts of a business event
The source is a screen — either a transaction screen (Sales Order, Shipment, Case) or a Generic Inquiry built specifically to expose the fields you want to watch. The Trigger Condition tab defines when the event fires: on record insert, on record update, on a specific field changing value, or on a schedule that re-runs the source GI on a timer. The Subscribers tab defines what happens next.
Acumatica ships three subscriber types out of the box: an email notification (built from a Notification Template, with placeholders bound to fields on the source screen), an Import Scenario (useful for pushing the triggering record's data into another entity inside the same instance), and a mobile push notification. Anything beyond that — a webhook, a queue message, a call to an external API — needs a fourth subscriber type: custom code, wired in through the SDK's business event extension point.
Pointing a Business Event straight at a transaction screen works, but it only exposes the fields already on that screen's data view. Building a purpose-made Generic Inquiry as the source gives you control over exactly which joined fields are visible to the trigger condition and to downstream notification templates — worth the extra ten minutes almost every time.
Row events vs. field-changed
"Record inserted" and "record updated" fire on every save that matches the condition filter — including saves where the field you actually care about didn't change. If a Sales Order gets touched five times before it ships, a business event set to "row updated" with a loose condition fires five times, not once. "Field changed" narrows this to a specific field crossing a specific value, which is the right choice for status transitions (order confirmed, case closed, shipment confirmed) and the wrong choice when you need to react to any of several fields.
Where custom code subscribers earn their keep
The built-in subscribers cover internal notifications and internal-to-internal data movement well. They don't cover calling an external REST endpoint, and they don't retry. A custom subscriber is a small class registered against the business event that runs your code when the event fires — this is where you'd place an HTTP call to Azure Functions, a message push to a queue, or a write to a custom log table. Because it runs synchronously inside the same request that saved the triggering record, keep it fast and defensive: a slow or failing external call in a custom subscriber will surface as a slow or failing save to the user sitting at the Sales Order screen.
This is the detail that catches people who assume Business Events behave like a message queue. They don't decouple you from the transaction — a custom subscriber that blocks on a slow external call blocks the user's save. If the downstream call can be slow or unreliable, queue the work from inside the subscriber and return immediately, rather than doing the call itself inline.
The execution log, and why it sometimes looks empty
Business Events keep an execution history you can inspect from the Business Events screen, but it's easy to misread: an email subscriber that successfully sends will still show up as "executed," even if the email itself bounced — the log tracks whether the subscriber ran, not whether its downstream effect (a delivered email, a completed import) succeeded. If you need proof that a webhook actually reached the other side, that has to be logged by the custom subscriber itself, not assumed from the Business Events screen.
A minimal custom subscriber
// Registered against the business event as a "Generic Handler" subscriber.
// Keep this fast: it runs inline with the record save that triggered it.
public class ShipmentConfirmedSubscriber : PXGraphExtension<ShipmentEntry>
{
public void OnBusinessEvent(SOShipment row)
{
if (row == null || row.Confirmed != true) return;
// Don't call the external system from here. Hand off to a queue
// table and let a separate processing screen or scheduled task
// do the actual HTTP call, with retry and logging.
PXDatabase.Insert<OutboundEventQueue>(
new PXDataFieldAssign("RefNbr", row.ShipmentNbr),
new PXDataFieldAssign("EventType", "ShipmentConfirmed"),
new PXDataFieldAssign("Status", "Pending"));
}
}
The pattern above — write to a local queue table instead of calling out directly — is the single most useful habit for business event subscribers. It turns an unreliable external dependency into a retry problem you control, instead of a save-time failure your users have to deal with.
When not to reach for Business Events
Business Events are the right tool for "notify someone" and "kick off an internal import" scenarios. They're the wrong tool for anything that needs guaranteed delivery, ordering across records, or retries with backoff — that's a job for a real outbound queue (Azure Service Bus, SQS, or even a polling processing screen reading from your own queue table) fed by a business event, not the business event acting as the queue itself.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.