I've been customizing Acumatica for years now — mostly for distributors and manufacturers in East Africa who need the platform bent around local tax rules, approval chains, and integrations that Acumatica's out-of-the-box modules were never going to cover. This guide is the overview I wish someone had handed me at the start: what the customization surface actually looks like, which layer to reach for when, and the habits that keep a customized instance upgradeable instead of turning it into a liability.
Everything in Acumatica customization sits on one idea: you never modify the base product. You extend it. The framework gives you extension points at every layer — data, business logic, and UI — and a packaging mechanism (the customization project) that layers your changes on top of the standard code at runtime. Respect that model and upgrades are boring. Fight it and every minor release becomes a rescue mission.
The three layers you extend
Acumatica screens are built from three things, and each has a matching extension mechanism:
- DACs (Data Access Classes) — C# classes that map to database tables and carry field metadata as attributes. You extend them with
PXCacheExtension<T>to add fields or override attributes. - Graphs (PXGraph) — the business logic controllers. One graph roughly equals one screen. You extend them with
PXGraphExtension<T>to add event handlers, actions, and data views, or to override existing methods. - Screens (ASPX pages or Modern UI TypeScript/HTML) — the presentation layer, edited through the Screen Editor in the customization project or, for Modern UI, through extension files that annotate the screen definition.
A useful mental model: the DAC says what the data is, the graph says what you're allowed to do with it, and the screen says how it's presented. Most real customizations touch all three — a new field needs a DAC extension, business logic around it needs a graph extension, and the field needs to be placed on the form.
Adding fields: DAC extensions
Say a client needs a "KRA PIN" (Kenyan tax ID) on the Customer record. You don't touch Customer.cs. You create an extension:
public sealed class CustomerExt : PXCacheExtension<Customer>
{
public static bool IsActive() => true;
[PXDBString(11, IsUnicode = true)]
[PXUIField(DisplayName = "KRA PIN")]
public string UsrKRAPin { get; set; }
public abstract class usrKRAPin : PX.Data.BQL.BqlString.Field<usrKRAPin> { }
}
Two conventions matter here. Custom fields are prefixed Usr — that's not just style, it's how Acumatica distinguishes your columns from theirs during upgrades, and the customization project's database script mechanism relies on it. And the IsActive() method lets the framework skip your extension entirely when a feature switch is off, which matters for performance and for multi-tenant instances where not every tenant wants your feature.
Behavior: graph extensions and events
Business logic lives in event handlers. Want to default the credit terms when a customer class changes? That's a FieldUpdated handler in a graph extension on CustomerMaint. Want to block saving an order missing your custom field? That's RowPersisting, where you can throw PXRowPersistingException or set an error on the field with PXUIFieldAttribute.SetError. The event model is the heart of the framework — FieldDefaulting, FieldVerifying, FieldUpdated, RowSelected, RowInserting, RowPersisting, RowPersisted — and knowing which one fires when is most of the job.
Putting expensive logic (BQL selects, external calls) in RowSelected. It fires constantly — on every refresh, every field change, every grid paint. RowSelected is for UI state: enabling fields, showing warnings. Anything heavier belongs in a more specific event.
Customization projects: the packaging layer
The Customization Projects screen (SM204505) is where everything gets bundled: DAC extensions, graph extensions, screen changes, new tables, site map entries, GIs, reports, even data records. A few things I've learned to do on every project:
- Keep the C# in a Visual Studio extension library, not in the Code Editor inside the project. You get real IntelliSense, source control, and unit tests. The customization project then just carries the compiled DLL (or references the files).
- One project per functional area, not one giant project. Publishing is all-or-nothing per project, and being able to unpublish the EDI integration without touching the tax customization has saved me more than once.
- Let the project generate database scripts for custom tables and columns rather than running SQL by hand — that's what makes the customization portable across tenants and environments.
Before you write code: GIs, workflows, and the low-code surface
A surprising fraction of "customization" requests need no C# at all. Generic Inquiries handle most reporting and list-screen needs. The workflow engine (the modern, state-machine-based one on the Workflow tab of a customization project) handles document statuses, conditional field states, and action visibility declaratively. Business Events plus import scenarios cover a lot of automation. My rule on client projects: exhaust the no-code tools first, because everything you build there survives upgrades with essentially zero maintenance. Code is for when the platform genuinely has no hook — and with each release, that surface shrinks.
Staying upgrade-safe
The customizations that survive version upgrades share the same traits: they use extensions rather than replacing base objects, they call base.Method() (or the delegate) inside overrides instead of swallowing base behavior, they avoid copying framework source into the project, and they don't depend on undocumented internals. Before every upgrade I republish all projects against the new version in a staging tenant and read the validation warnings — the publish-time validator catches a good share of breakage before users ever see it.
Wrapping up
Acumatica's customization model rewards discipline: DAC extensions for data, graph extensions for behavior, screen/workflow tools for presentation and state, all packaged in customization projects that layer cleanly over the base product. Start with the no-code surface, move to extensions only when you must, keep your C# in a real solution under source control, and treat "will this survive the next upgrade?" as a design constraint from day one — not a question you ask afterwards.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.