PXFormula looks like a small convenience feature the first time you use it - a declarative way to say "this field equals that expression" without writing a FieldUpdated handler. The part that trips developers up is evaluation context: PXFormula re-evaluates whenever any field it references changes, not once, and it evaluates against the row's current in-cache state, not a frozen snapshot - which means write order and which fields are "ready" at evaluation time genuinely matter.
PXFormula subscribes to FieldUpdated on every referenced field, not just the decorated one
Decorating a field with PXFormula doesn't just compute a value once on load. It registers the field as dependent on every field referenced inside the formula expression, and re-runs the calculation any time any of those dependencies changes - which is exactly the behavior you want for something like extended price, and exactly the behavior that surprises people who expected it to run once at row creation:
public sealed class SOLineExt : PXCacheExtension<SOLine>
{
public static bool IsActive() => true;
[PXDBDecimal(2)]
[PXFormula(typeof(Mult<SOLine.orderQty, SOLine.unitPrice>))]
[PXUIField(DisplayName = "Extended Price")]
public decimal? UsrExtPrice { get; set; }
public abstract class usrExtPrice : PX.Data.BQL.BqlDecimal.Field<usrExtPrice> { }
}
Change OrderQty, UsrExtPrice recalculates. Change UnitPrice, same thing. Neither field needs to know the other triggers a dependent recalculation - PXFormula wires that dependency graph up for you from the expression alone, which is the actual value of using it over a hand-written FieldUpdated handler on each contributing field.
Chained formulas evaluate in dependency order, which is not always declaration order
When one formula field feeds another - a line total feeding a header total, say - the framework resolves the dependency chain and evaluates in the order the chain actually requires, not necessarily the order the fields are declared in the DAC. This mostly works transparently, but breaks down the moment a dependency is indirect, through code rather than through the formula expression itself:
// This formula's evaluation context sees OrderQty and UnitPrice's
// CURRENT cache values. If a FieldUpdated handler elsewhere is
// about to adjust UnitPrice based on some other condition, and that
// handler runs AFTER this formula evaluates, UsrExtPrice reflects the
// pre-adjustment price - a stale calculation, not a bug in PXFormula itself.
[PXFormula(typeof(Mult<SOLine.orderQty, SOLine.unitPrice>))]
public decimal? UsrExtPrice { get; set; }
If your own code changes UnitPrice programmatically from inside a FieldUpdated handler on some other field - a discount override, say - PXFormula's dependency tracking generally re-fires correctly because the underlying field change goes through the same cache API. Where this breaks is when a value is set via a path that bypasses normal field-update notification, like a direct bulk update outside the cache API. If a calculated field looks stale after a programmatic change, check whether the value was actually set through cache.SetValueExt (or the DAC property setter, in cache-tracked code) rather than assigned in a way that skips notification.
Conditional evaluation with PXFormula's condition types
PXFormula supports conditional expressions - Selector<>-style branching so a formula only applies under certain conditions, leaving the field's existing value untouched otherwise. This matters because an unconditional formula overwrites a manually-entered value every time any dependency changes, which is rarely what you want once a user has typed an override:
// Only recalculate the suggested price if the user hasn't manually
// overridden it - IsManualPrice guards the formula from clobbering
// a deliberate user entry on every quantity change
[PXFormula(typeof(Switch<
Case<Where<SOLine.usrIsManualPrice, Equal<False>>,
Mult<SOLine.orderQty, SOLine.unitPrice>>,
SOLine.usrExtPrice>))]
public decimal? UsrExtPrice { get; set; }
Without a guard like this, I've watched a "smart" price calculation field fight a warehouse clerk's manual price override on every quantity adjustment, because the formula kept winning the last-write race on every recalculation - a support ticket that took longer to explain than to fix, once someone actually looked at the formula's unconditional dependency list.
A formula field can be bound or unbound - decide based on whether you need to query it
Formula fields are commonly PXDBDecimal-backed (persisted, queryable in BQL and reports) but can just as validly be unbound (PXDecimal, computed on the fly, never stored). Persist it when other code or reports need to filter or join on the value; keep it unbound when it's purely a display convenience recomputed from fields already on the row, since persisting a value that's trivially derivable from other stored fields is redundant storage that can drift out of sync if the formula ever changes without a data migration to recompute existing rows.
Wrapping up
PXFormula's evaluation context is the current in-cache state of every field it depends on, re-evaluated on every dependency change, not a one-time calculation - which is exactly right for keeping derived fields in sync and exactly the thing to guard against when a formula shouldn't clobber a manual override. Use conditional formulas when user input needs to coexist with automatic calculation, and think carefully about whether a derived value needs to be persisted at all before defaulting to a bound PXDBDecimal field for it.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.