Acumatica · Customization

Acumatica Graph Extension Patterns

A deep dive into graph extensions in Acumatica — when to extend, when to wrap, when to override, with concrete patterns for the screens you will customise most.

John Kihiu12 min read

PXGraphExtension<T> is the workhorse of Acumatica customization — nearly every piece of custom business logic I write lands in one — but there's a real difference between graph extensions that age well across upgrades and ones that turn into landmines the moment Acumatica ships a new base version. This is a tour of the patterns I actually reach for, and the ones I've learned to avoid the hard way.

The basic shape: extending events, not replacing them

A graph extension attaches to a base graph and can add data views, add actions, and — most commonly — add or override event handlers. The convention-based method-naming pattern (an underscore method named after the event and generic-typed to the row) is how the framework wires your handler in without you registering anything explicitly:

C#
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
    public static bool IsActive() => true;

    protected virtual void _(Events.RowPersisting<SOOrder> e)
    {
        if (e.Row == null) return;
        if (e.Row.OrderTotal > 1_000_000m && string.IsNullOrEmpty(e.Row.UsrDirectorApproval))
        {
            e.Cache.RaiseExceptionHandling<SOOrderExt.usrDirectorApproval>(
                e.Row, null, new PXSetPropertyException("Orders over 1,000,000 require director approval"));
            e.Cancel = true;
        }
    }
}

This additive form — reacting to an event without touching base behavior — is the safest pattern in the whole framework, because it can never conflict with what the base graph does; it only runs alongside it.

Overriding base methods: call base, always

Sometimes you genuinely need to change base behavior, not just react around it — overriding an action's implementation or a graph method. The rule that keeps this upgrade-safe: call the delegate (or base.Method()) unless you have a specific, documented reason not to:

C#
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
    [PXOverride]
    public void ReleaseFromCreditHold(SOOrder doc, Action<SOOrder> baseMethod)
    {
        // Custom pre-check
        if (doc.CreditHoldOverrideReason == null)
            throw new PXException("An override reason is required to release from credit hold.");

        baseMethod(doc); // always call through — this is what keeps the base
                          // release logic (GL impacts, notifications, status
                          // transitions) intact across version upgrades
    }
}

Swallowing the base call — never invoking baseMethod — is how customizations silently lose functionality the base graph was quietly relying on other code paths to have run. I've inherited instances where a base method's side effects (a status update three fields away from anything the override author was thinking about) simply stopped happening, and nobody noticed until a downstream report went wrong months later.

Multiple extensions on one graph: ordering is real and mostly out of your control

Large instances accumulate several independent PXGraphExtension classes on the same base graph — one per customization project. When two extensions both handle the same event on the same DAC, execution order depends on customization project publish order, which is not something you want to depend on for correctness. If extension A's RowPersisting handler needs to run before extension B's, don't rely on publish order — either consolidate the logic into one handler, or have one extension explicitly check whether the condition the other extension cares about has already been handled, using cache state rather than assumed ordering.

IsActive() is your feature-flag mechanism — use it

Every graph extension (and DAC extension) should implement IsActive() deliberately, not just return true unconditionally out of habit. Tie it to a feature license, a company-level setting, or a customization flag, and an extension you don't currently need simply doesn't attach its handlers — no runtime cost, no risk of interfering with unrelated logic on multi-tenant or multi-branch instances where not every entity wants your customization active.

Adding data views and actions cleanly

A graph extension can add its own PXSelect views and PXAction buttons without touching the base graph's declared views at all — the cleanest form of extension, because there's nothing to conflict with:

C#
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
    public PXSelect<SOOrderApprovalLog,
        Where<SOOrderApprovalLog.orderNbr, Equal<Current<SOOrder.orderNbr>>>> ApprovalLog;

    public PXAction<SOOrder> ViewApprovalHistory;
    [PXUIField(DisplayName = "Approval History")]
    [PXButton]
    protected virtual IEnumerable viewApprovalHistory(PXAdapter adapter)
    {
        // navigate to a related screen, or throw a PXRedirectRequiredException
        throw new PXRedirectRequiredException(Base, "Approval History") { Mode = PXBaseRedirectException.WindowMode.NewWindow };
    }
}

When a graph extension is the wrong tool

Graph extensions attach behavior to an existing screen's graph. When the requirement is actually a brand new screen with its own workflow — not a modification of an existing one — subclassing PXGraph directly (or building an entirely new graph) is correct instead, and trying to force new-screen logic into an extension of an unrelated existing graph produces a confusing, hard-to-navigate customization. The PXGraph vs PXGraphExtension decision deserves its own dedicated look, but the short version: extend when you're modifying behavior on a screen that already exists and should keep existing; write a new graph when you're building something genuinely new.

Wrapping up

The patterns that keep graph extensions upgrade-safe: prefer additive event handlers over method overrides where possible; when you must override, always call the base delegate; gate every extension with a deliberate IsActive(); and don't rely on cross-extension execution order for correctness. The extensions that survive years of Acumatica upgrades without a rescue mission are, almost without exception, the ones that follow these rules from the first commit rather than getting refactored into them after the first upgrade breaks something.

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.