Acumatica's graph is not asynchronous in the JavaScript sense — there is no promise queue — but it does have a real event loop of its own: RowSelecting, RowInserting, RowUpdated, FieldUpdated, and FieldVerifying fire in a defined order as PXCache processes a row, and each of those handlers can trigger more cache operations that queue more events. Understanding that loop is the difference between a snappy data entry screen and one where typing a quantity visibly pauses the UI.
The order that actually fires
For a single field edit in the UI, the practical sequence is: FieldVerifying (validation, can cancel) → FieldUpdating (value transformation) → cache value set → FieldUpdated (side effects: recalculating dependent fields) → RowUpdated (row-level consistency) → persistence events on save. Each FieldUpdated handler that calls Cache.SetValueExt on another field re-enters this same loop for that field before the outer handler returns. A chain of three fields each updating the next is three nested passes through the loop for one keystroke.
protected virtual void SOLine_Qty_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e)
{
var row = (SOLine)e.Row;
if (row == null) return;
// Recalculates ExtPrice, which itself has a FieldUpdated
// that recalculates line discount, which recalculates order total...
cache.SetValueExt<SOLine.extPrice>(row, row.Qty * row.UnitPrice);
}
None of this is wrong by itself — it is exactly how dependent-field recalculation is supposed to work. It becomes a performance problem at scale: a grid paste of 200 lines fires this cascade 200 times, and if the order total recalculation at the end of the chain re-sums every line each time rather than incrementally, you get roughly O(n²) behavior on what should be a linear paste operation.
Spotting a runaway cascade
The Trace screen (or the Request Profiler's detailed trace) shows event handler execution as a nested, timestamped list. A healthy field update shows a shallow tree — two or three levels deep. A cascade shows the same handler names repeating at increasing depth, and the total time for a "simple" quantity change running into the hundreds of milliseconds is the tell.
The most common cause of quadratic cascades I have removed from client customizations: an order total recalculated by re-summing every line inside SOLine_Qty_FieldUpdated, rather than deferring the total recalculation to RowUpdated or, better, to a single pass after a multi-row paste completes. Move expensive aggregate work out of the per-field handler and into a point that fires once per save, not once per keystroke.
Breaking cascades deliberately
Two tools stop unnecessary re-entrancy:
cache.IsDirtyand value-equality checks before callingSetValueExt— if the new value equals the current value, skip the set entirely and you skip the whole downstream cascade for that field.PXCache.ClearQueryCacheObsoleteand batch-mode flags (cache.AllowUpdate = falsearound bulk operations, restored afterward) which suppress per-row UI-oriented events during programmatic bulk changes such as an import or mass-update processing screen, where nobody is watching field-by-field animation anyway.
Wrapping up
Treat FieldUpdated/RowUpdated chains as a real event loop with real re-entrancy cost, not a free hook to attach logic to. Trace the nesting depth before assuming a slow grid is a database problem, and move aggregate recalculation out of the per-keystroke path whenever the operation is really "recompute once at the end."
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.