Acumatica's Business Events let you react to a data change — a new case, a released sales order, a bill approval — by sending a notification email or updating a field, all without writing a line of code. They're the first thing I reach for when a client asks "can the system just tell someone when X happens", and they're also the thing I see most often stretched past what they were built for. Knowing where that line sits is the actual skill.
What a Business Event actually is
A Business Event is not its own independent engine — it's a thin automation layer sitting on top of a Generic Inquiry. To build one, you need a GI that already exposes: the fields you want to monitor, the condition (trigger) that should fire the event, and whatever action-schedule or email-notification fields the event will use when it fires. Acumatica polls that GI-backed condition and, when a record matches, runs the configured action — usually an email, sometimes a field update. If you've never had to write a GI with a filter condition before setting up a Business Event, that's unusual; the GI is doing the actual detection work, the Business Event is just wiring the trigger to an action.
The part nobody mentions in the demo
Because a Business Event is GI-driven, it inherits the GI's relationship to the database: it's evaluated against committed data, on a schedule, not synchronously as part of the transaction that changed the record. In practice this means a Business Event is a polling reaction, not an interception. The record changes, the change commits, and at some later point (seconds to minutes depending on how the event is scheduled) the condition gets evaluated and the action fires. That's fine for "notify the AP clerk when a bill over $10,000 gets approved" — nobody needs that email synchronously. It is not fine for anything where you need to block, modify, or validate the operation before it commits, or where a few minutes of lag changes the outcome.
A Business Event fires after the fact. It cannot reject a save, force a field to a different value before the row is written, or prevent a duplicate from being created in the first place. If the requirement is "don't let this happen," you're not looking for a Business Event — you're looking for a RowUpdating or RowInserting handler that can cancel the operation with a PXSetPropertyException before it commits.
A real example: Business Event feeding an Import Scenario
One of the more useful patterns I've built repeatedly: a Business Event watches for a condition — say, a new opportunity assigned to a sales team — and instead of just emailing someone, its action triggers an Import Scenario that creates and assigns a follow-up task automatically. The GI supplies the trigger and the source data, the Import Scenario supplies the "go create this related record" logic, and the whole thing runs with zero custom code. This is exactly the kind of automation Business Events are for: turning a data condition into a downstream record, on a delay measured in minutes, with no risk if it runs a little late.
When you drop to a graph or DAC event handler
The moment the requirement needs any of the following, stop trying to force it into a Business Event and write the handler instead:
- Synchronous validation or blocking. RowUpdating, RowInserting, or a field's FieldVerifying handler run inside the transaction and can reject it. A Business Event cannot.
- Mid-transaction reaction. RowUpdated and RowInserted fire immediately after the in-memory change, well before whatever polling interval a Business Event runs on. If "immediately" matters, code wins.
- Conditional defaults. FieldDefaulting lets you compute a default based on other fields on the same or related records at the moment the field is touched — there's no GI-trigger equivalent for this.
- Logic too conditional for a GI filter. A GI trigger condition is a filter expression. Once the rule involves branching logic, lookups against multiple related entities, or anything you'd naturally write as an if/else chain, it belongs in a graph extension, not a GI you're contorting into an if-statement.
public class SOOrderEntry_RowEventsExt : PXGraphExtension
{
protected virtual void _(Events.RowUpdating e)
{
var row = (SOOrder)e.Row;
var newRow = (SOOrder)e.NewRow;
if (row == null || newRow == null) return;
// Block the change synchronously, before commit -
// a Business Event can only react after this row is saved.
if (row.Status == "Completed" && newRow.OrderTotal != row.OrderTotal)
{
throw new PXSetPropertyException(
"Cannot change the total of a completed order.");
}
}
protected virtual void _(Events.RowUpdated e)
{
var row = (SOOrder)e.Row;
if (row == null) return;
// Reacts in-transaction, not on the next Business Event
// polling cycle - use this when timing actually matters.
if (row.Status == "Open" && row.CreditHold == true)
{
PXTrace.WriteInformation($"Order {row.OrderNbr} flagged on hold.");
}
}
}
The decision rule
Ask two questions. Does the reaction need to happen before the transaction commits, or is "eventually, within a few minutes" acceptable? And is the trigger condition expressible as a GI filter, or does it need branching logic across related entities? If the answer is "after commit is fine" and "yes, a GI filter can express it," build a Business Event — it's less code, no deployment, and anyone on the functional side can maintain it later. If either answer is no, write the DAC or graph event handler. Don't build a Business Event on a GI so contorted that it takes longer to design than the equivalent ten lines of C#, and don't write a custom handler for a plain "email someone when this field changes" requirement that a GI and a Business Event already solve for free.
Wrapping up
Business Events are a real no-code automation layer, but they are GI-driven and post-commit by construction — they poll a Generic Inquiry's trigger condition and react after the row is already saved. That makes them excellent for notifications and downstream record creation like the Business-Event-to-Import-Scenario pattern, and wrong for anything that needs to validate, block, or react inside the transaction itself. When the requirement needs synchronous interception, go straight to RowUpdating, RowInserted, or FieldDefaulting on the DAC or graph — don't spend a day trying to make a GI trigger do a code event handler's job.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.