Acumatica · Architecture

Acumatica Plugin Architecture Explained

How the Acumatica plugin architecture works — auto-discovery of graphs, DACs, and reports, with the conventions for organising a codebase that scales to dozens of custom screens.

John Kihiu12 min read

Acumatica does not have a "plugin API" in the WordPress sense. What people call the plugin architecture is really the customization/extension model: you drop compiled types and published customization content into the application, and the framework discovers them by convention at startup. Understanding what gets auto-discovered — and what has to be registered by hand — is what keeps a codebase with dozens of custom screens from turning into a pile of guesswork.

What gets discovered automatically

When the site application domain loads, Acumatica scans the assemblies in the Bin folder and reflects over them. Any type deriving from PXGraph, PXGraphExtension<>, or PXCacheExtension<> is picked up without you registering it anywhere. That is the core of the "plugin" behaviour: a graph extension in your assembly is active the moment the DLL is present, because the framework asks each cache and each graph for the extensions declared against it. There is no manifest listing your extensions the way an ASP.NET app lists middleware — the base type parameter is the registration.

C# · GRAPH EXTENSION
// Present in Bin ⇒ discovered. No registration call anywhere.
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
    // Runs only when the customization package is *published* if it
    // touches DB/screen content; pure code extensions are live once
    // the DLL loads and the app domain recycles.
    public static bool IsActive() => true;

    protected void _(Events.RowSelected<SOOrder> e)
    {
        if (e.Row == null) return;
        // custom behaviour on the Sales Orders screen
    }
}

The IsActive() hook is the one lever you get over discovery. Return false and the framework skips the extension entirely — that is how you scope an extension to a feature toggle or a specific tenant configuration without commenting out code.

What needs explicit registration

Not everything is convention-driven. DACs (data access classes) for custom tables are discovered by reflection, but the table behind them only exists if the customization project's Database Scripts or a Custom Table definition created it. Reports (.rpx), generic inquiries, and screen (.aspx) content are not code at all — they live in the customization package and are applied to the site only when you publish that package. This is the split that trips people up: your C# is "live" as soon as the assembly loads, but anything that changes the database schema or the site's file tree is inert until publish.

Two different lifecycles in one project

Compiled extensions (graphs, DACs, attributes) and package content (screens, reports, DB scripts) ship together but activate differently. A hotfix that is pure C# can go out with an assembly swap and an app-pool recycle; anything schema-touching needs a full publish. Plan releases around which half you changed.

Organising a codebase that scales

Once you pass a handful of screens, folder-by-technical-layer stops helping and folder-by-feature starts. Group the graph extension, DAC extensions, custom DACs, attributes, and helper classes for one business area together, so a single functional change lives in one place. The convention I settle on across projects:

TEXT · PROJECT LAYOUT
MyCompany.Distribution/
  Warehousing/
    Descriptor/        // attributes, selectors, constants
    DAC/               // custom DACs + PXCacheExtensions
    Graph/             // PXGraphExtensions, custom PXGraphs
  Pricing/
    DAC/
    Graph/
  Common/              // shared attributes + base classes

Namespaces should track the folders, but the assembly name is what matters for publishing: keep one assembly (or a small, stable set) so the customization package references stay simple. Splitting into many assemblies buys you nothing here — discovery works across all of Bin — and costs you dependency headaches at publish time.

The Usr prefix on base tables

Custom fields on stock DACs must start with Usr

When you add a field to a built-in table like SOOrder through a PXCacheExtension, the field name has to begin with Usr. The framework reserves that prefix for customer fields so upgrades can distinguish them from Acumatica's own columns and never collide. Skip the prefix and the field either fails to persist or breaks on the next upgrade.

How extensions layer on top of each other

Multiple extensions can target the same graph, and they run in a defined order. By default the order is undefined-but-stable; when you need to force it, an extension overrides GetExtensionSchedule() or declares a dependency so that, say, your extension always runs after an ISV's. This matters the moment you install a third-party package on a screen you have already customized — both extensions are discovered, both are active, and their event handlers stack. Test that combination explicitly rather than assuming your handler runs alone.

C# · ORDERING EXTENSIONS
public class SOOrderEntryExt : PXGraphExtension<
    ThirdParty.SOOrderEntryExt,   // run after this one
    SOOrderEntry>                 // on this graph
{
    protected void _(Events.RowSelected<SOOrder> e)
    {
        // safe to read state the ISV extension has already set
    }
}

Why this model holds up across upgrades

The reason the extension model survives version upgrades is precisely that you never modify base source. Your code sits in a separate assembly, hooks documented extension points, and is re-discovered against the new base each time. When an upgrade breaks something, it breaks at a visible seam — a renamed field, a changed signature — rather than silently, because your extension had to reference the base type by name. That is the whole trade: you accept the ceremony of extensions and Usr prefixes in exchange for upgrades that fail loudly and locally instead of quietly.

ArtifactDiscoveryActivates on
Graph / graph extensionReflection over BinAssembly load + recycle
DAC / cache extensionReflection over BinAssembly load (table must exist)
Custom table / DB scriptCustomization packagePublish
Screen (.aspx), report (.rpx), GICustomization packagePublish

Wrapping up

Think of the plugin architecture as two channels feeding one running app: compiled types the framework finds by convention, and package content you apply by publishing. Keep your code in feature-shaped folders inside a stable assembly, prefix every custom field with Usr, and always test how your extensions layer against ISV packages on the same screen. Get those right and adding the thirtieth custom screen feels the same as adding the third.

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.