Acumatica · Customization

Acumatica Customization — The Definitive Guide

The full picture of an Acumatica customization project: what it contains, how it is structured, how it survives upgrades, and the decisions that determine whether it scales to 50 screens or dies at 5.

John Kihiu17 min read

An Acumatica customization is a packaged application layer. The official documentation calls it a "customization project"; the community calls it a "CUSP" or simply "the package". Whatever the name, it is the unit of deployment for everything you build on top of Acumatica ERP. This guide is everything I have learned shipping customization projects across finance, distribution, and manufacturing clients in Kenya, Rwanda, Tanzania, Zambia, and South Africa.

1. What is in a customization

A customization is a ZIP that Acumatica publishes into a tenant. It can include any of:

The crucial fact: a customization is a single atomic unit. You publish it; the entire bundle is applied. There is no partial publish (in production, anyway).

2. The Customization Project Editor

The Customization Project Editor is the Visual Studio-like IDE inside Acumatica. It is fine for the first 20 files. Past that, you should be in Visual Studio or Rider with the Acumatica add-in, and using source control. The CPE is for packaging, not authoring.

3. Project structure that scales

Beyond a few screens, a flat customization becomes unmaintainable. The structure I use for any project that will live past one release:

FOLDER LAYOUT
AcumaticaMyClient/
├── AcumaticaMyClient.sln
├── src/
│   ├── AcumaticaMyClient.Core/         // DACs, business logic, services
│   ├── AcumaticaMyClient.Graph/        // Graph extensions
│   ├── AcumaticaMyClient.Web/          // ASPX, Modern UI extensions
│   └── AcumaticaMyClient.Reports/      // RDLC and schemas
├── test/
│   └── AcumaticaMyClient.Tests/        // xUnit + Acumatica Unit Test Framework
└── db/
    └── migrations/                     // SQL change scripts, versioned

4. DACs — the heart of Acumatica

A DAC (Data Access Class) is a C# class that maps to a database table. The mapping is attribute-based:

C# · MINIMAL DAC
[Serializable]
[PXCacheName("My Entity")]
public class MyEntity : IBqlTable
{
    #region RecordID
    [PXDBIdentity(IsKey = true)]
    public virtual int? RecordID { get; set; }
    public abstract class recordID : PX.Data.BQL.BqlInt.Field<recordID> { }
    #endregion

    #region Name
    [PXDBString(60, IsUnicode = true)]
    [PXUIField(DisplayName = "Name")]
    public virtual string Name { get; set; }
    public abstract class name : PX.Data.BQL.BqlString.Field<name> { }
    #endregion
}

Notice the dual field pattern: a property and a matching abstract class that BQL uses for type-safe queries. This is the Acumatica way, and skipping it will bite you within a week.

5. Graph extensions vs new graphs

You almost always want to extend an existing graph rather than create a new one. A graph extension inherits the entire screen and lets you add fields, tabs, actions, and event handlers without re-implementing anything. The base graph does all the heavy lifting (CRUD, validation, persistence); your extension customises behaviour.

C# · GRAPH EXTENSION
public class ARInvoiceExt : PXGraphExtension<ARInvoiceEntry>
{
    [PXOverride]
    public IEnumerable<ARInvoice> GetInvoices()
    {
        // additional logic before calling the base
        return Base.GetInvoices();
    }
}

6. Field-level customisation on existing DACs

To add a field to a built-in DAC (e.g. ARInvoice), declare a DAC extension. Acumatica uses the Usr prefix convention:

C# · DAC EXTENSION
[PXTable(IsOptional = true)]
public class ARInvoiceExt : PXCacheExtension<ARInvoice>
{
    #region UsrExternalRef
    [PXDBString(40)]
    [PXUIField(DisplayName = "External Ref")]
    public virtual string UsrExternalRef { get; set; }
    public abstract class usrExternalRef :
        PX.Data.BQL.BqlString.Field<usrExternalRef> { }
    #endregion
}

7. Events — the right tool for the job

Acumatica graphs raise dozens of row events. The ones you will use 90% of the time:

Performance is in the event handler RowSelected fires for every row on every UI render. Anything heavy there (loops, remote calls) will make the screen feel sluggish. Push heavy work to RowPersisting or to a graph action.

8. The Modern UI layer

Since 2025 R2, the Modern UI is the default. Customizing it is a different craft from the classic UI:

9. Source control and packaging

Customizations should live in Git. The binary ZIP should be buildable from source. The CPE is for one-off inspection; CI/CD should produce the package.

10. Testing

Acumatica ships a Unit Test Framework. Use it. A customization with even minimal coverage is dramatically easier to upgrade. At minimum:

11. Upgrade survival

Three habits keep your customization upgrade-safe:

  1. Wrap everything in extensions. Never re-implement a base graph.
  2. Wrap DAC additions in PXCacheExtension. Never edit base DACs.
  3. Use the Usr prefix on all new fields. Acumatica will not collide.

12. ISV solutions — when to go commercial

If your customization is genuinely a product (multi-tenant, distributed, versioned, supportable), you publish it as an ISV solution on the Acumatica Marketplace. Different packaging, different pricing, different support obligations. Worth the path when you have customers beyond one.

13. Common production issues

  1. Forgetting to publish the database script. The code deploys, the field does not exist, the tenant 500s.
  2. Modifying a base DAC. Your change gets blown away on upgrade.
  3. Heavy logic in RowSelected. The screen becomes a slideshow at 10k records.
  4. Hard-coded tenant IDs. The next tenant cannot use your customization.
  5. No source control on the customization ZIP. A laptop dies, a year of work dies with it.

Related reading

Going deeper: production-grade patterns

The patterns above cover the basics. In production, the same patterns have to survive three things: scale, edge cases, and the next Acumatica upgrade. Here are the patterns that distinguish a working customisation from a great one — the ones I have applied to every client project in East and Southern Africa, and the ones that make the difference between a customisation the user trusts and a customisation they curse.

Defensive coding for the unexpected

Production is where the assumption dies. Every customisation that "works in test" fails in production the first time a customer name has a special character, an invoice is in a foreign currency, or a record has a null in a field you thought was required. The defensive habit is to explicitly handle the null, the empty, the special character, and the foreign currency in every event handler and every code path. The cost is 20% more code. The payoff is 95% fewer production tickets.

Three patterns I apply everywhere:

C# · DEFENSIVE PATTERN
public class DefensiveExt : PXGraphExtension<BaseGraph>
{
    protected void _(Events.RowSelected<MyDAC> e)
    {
        var row = e.Row;
        if (row == null) return;                          // null-safe
        var ext = row.GetExtension<MyDACExt>();
        if (ext == null) return;                         // null-safe extension
        var value = ext.UsrField ?? "DEFAULT";           // null-coalesce
        var ok = decimal.TryParse(value, out var n);    // try-parse
        if (!ok) { /* handle */ }
    }
}

Performance: the patterns that scale

Five performance patterns I apply on every customisation, in order of impact:

  1. Move heavy logic out of RowSelected. Push validation to RowPersisting, side effects to a graph action triggered by a button. RowSelected fires for every row on every render.
  2. Index the join columns. Every BQL Where<> filter needs an index. Check the execution plan before you ship.
  3. Filter at the GI, not the UI. A GI that returns 5 million rows and filters in the presentation layer will time out. Push filters into the Conditions tab.
  4. Batch the work. Loop with 1,000 calls is slow; loop with 10 calls of 100 records is fast. Batch where you can.
  5. Cache the static. Tax schedules, account lists, and other static reference data can be cached for the lifetime of the app pool. Reduce the database load.

For the full performance playbook, see the performance tuning guide and the SQL Server indexing guide.

Upgrade survival

The customisation that breaks on the next Acumatica upgrade is the one that took a shortcut. The patterns that survive:

C# · USR PREFIX CONVENTION
// Base field — Acumatica owns this
[PXDBString(40)]
public string RefNbr { get; set; }

// Your field — always Usr prefix, never collides
[PXDBString(40)]
[PXUIField(DisplayName = "External Ref")]
public string UsrExternalRef { get; set; }

// Your DAC extension — soft extension, survives table drops
[PXTable(IsOptional = true)]
public class MyDACExt : PXCacheExtension<MyDAC>
{
    #region UsrCustomField
    [PXDBString(60)]
    public string UsrCustomField { get; set; }
    public abstract class usrCustomField :
        PX.Data.BQL.BqlString.Field<usrCustomField> { }
    #endregion
}

Testing: the habit that pays for itself

If you are not testing your customisation with the Acumatica Unit Test Framework, you are running blind. The framework ships with every installation, costs nothing, and pays for itself the first time an upgrade changes a method signature on you. The minimum coverage:

For the full test framework walkthrough, see the unit test framework guide.

Operations: what to do after the customisation is live

A customisation is not "done" when it ships. It is "done" when it has run in production for a quarter without a critical incident. The operational habits that get you there:

For the broader operational patterns, see the monitoring guide and the licence concurrency guide.

The migration off the old customisation

Every customisation is eventually replaced. Plan for that day from the start. The patterns:

For the broader migration patterns, see the data migration guide.

Wrapping up

That is the working approach I use on Acumatica projects. The same patterns show up whether you are in Nairobi, Johannesburg, Kigali, Lusaka or Harare — and they are the things that keep work moving when an upgrade lands at 6 PM on a Friday. If you are stuck on something specific, reach out or keep reading through the rest of the Acumatica blog.