Acumatica · Field Service

Acumatica Field Service — Extension Patterns

Acumatica Field Service — Extension Patterns — a complete, field-tested reference by John Kihiu, Acumatica developer in Nairobi.

John Kihiu12 min read

Acumatica Field Service — Extension Patterns is one of those Acumatica customisations that every team eventually needs and almost no team does well the first time. The Acumatica module ships with a complete set of fields, but every real business has a field the framework does not provide — a reference number, a department code, a tenant-specific workflow flag. Adding that field cleanly is what this guide is about.

To add a field, a UDF, or a separate DAC

Before you write any code, decide which of the three patterns fits. The wrong choice costs you at every upgrade; the right choice disappears into the platform.

PatternWhen to useUpgrade cost
User-Defined Field (UDF)End users should be able to add the field without a developerLow — UDFs survive upgrades
DAC extension (Usr field on existing DAC)Code needs to read/write the field programmaticallyLow — extensions are the standard pattern
Separate DAC with a relationshipThe field has its own workflow, validations, or sub-recordsMedium — a new DAC means a new table and a new graph
Decision short-circuit

If you find yourself wanting to add more than three fields to the same screen at once, stop. A separate DAC with a relationship is almost always the right move. Fields on a DAC should be the obvious ones the screen needs; complex sub-entities belong in their own table.

The DAC extension pattern

For the 80% case — adding a custom field to a built-in screen — the DAC extension is the right tool. The pattern has three moving parts: the extension class, the graph extension to expose the field, and the screen configuration to show it.

C# · DAC EXTENSION
[PXTable(IsOptional = true)]
public class FSSrvOrdTypeExt : PXCacheExtension<FSSrvOrdType>
{
    #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

    #region UsrCustomCategory
    [PXDBString(20)]
    [PXUIField(DisplayName = "Custom Category")]
    [PXStringList(new[] { "STANDARD", "PRIORITY", "INTERNAL" },
        new[] { "Standard", "Priority", "Internal" })]
    public virtual string UsrCustomCategory { get; set; }
    public abstract class usrCustomCategory :
        PX.Data.BQL.BqlString.Field<usrCustomCategory> { }
    #endregion
}

The pattern above has three Acumatica-specific things that are easy to get wrong:

  1. The Usr prefix. Every custom field on a built-in DAC must start with Usr. Acumatica uses this prefix to separate your fields from base fields, and the upgrade tool relies on it to know what to preserve.
  2. The PXTable(IsOptional = true) attribute. This tells the framework the extension is a "soft" extension — if the database table does not have the column, no error. Without this, an upgrade that drops a table would crash the customisation.
  3. The abstract class for BQL. The dual-field pattern (property + abstract class) is how Acumatica queries work. The property holds the value at runtime; the abstract class is the type token the BQL compiler uses. Skipping the abstract class means you cannot query the field in BQL.
The field did not persist

If you add a field and the screen lets you type a value but the value disappears on save, the column probably does not exist in the database. The most common cause is forgetting to publish the database script along with the customization. Always publish both as a pair.

Wiring the field to the screen

For the Modern UI, the screen configuration model picks up your new field automatically once the customization is published. For the classic UI, you need to add the field to the ASPX layout explicitly. Either way, the work is the same — declare the field, declare its container (a tab, a section, a grid), and let the framework render it.

ASPX · ADD FIELD TO SCREEN
<px:PXFormView ID="form" runat="server" DataSourceID="ds" DataMember="FSSrvOrdType">
  <Template>
    <px:PXLayoutRule runat="server" StartColumn="True" />
    <px:PXTextEdit runat="server" ID="edUsrExternalRef" DataField="UsrExternalRef" />
    <px:PXDropDown runat="server" ID="edUsrCustomCategory" DataField="UsrCustomCategory" />
  </Template>
</px:PXFormView>

For the Modern UI, the equivalent is a screen configuration entry in the Screen Configuration screen. The drag-and-drop editor lets you place the field on any tab; the resulting JSON is committed to the customisation package.

Reading and writing the field

Once the field is on the screen, you need to read it (for filtering, reporting) and write it (for automation). The pattern for both is the same: get the cache, cast to the extension, access the field.

C# · READ / WRITE THE CUSTOM FIELD
public class ServiceOrderEntryExt : PXGraphExtension<ServiceOrderEntry>
{
    protected void _(Events.RowPersisting<FSSrvOrdType> e)
    {
        var row = e.Row;
        if (row == null) return;
        var ext = row.GetExtension<FSSrvOrdTypeExt>();
        if (ext?.UsrExternalRef == null)
            ext.UsrExternalRef = $"REF-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid().ToString()[..8]}";
    }
}

For a deeper treatment of BQL syntax and the dual-field pattern, see the DAC fundamentals article and the cache attach pitfalls guide.

Migrating data into the new field

Adding the field is half the work. The other half is getting existing data into it. Three options, in order of complexity:

  1. Import Scenario. Define a mapping from a CSV (or another table) to the new field. Run it once. The audit trail tells you what was set.
  2. Graph event handler. Set the field in RowPersisting based on a lookup against another table. The field gets populated as records are touched.
  3. SQL update. A direct UPDATE statement. Fast, but no audit trail and no upgrade safety. Use only when you have full control of the database.
SQL · DIRECT UPDATE (USE WITH CARE)
UPDATE ar
SET ar.UsrExternalRef = src.ExternalRefNbr
FROM dbo.FSSrvOrdType ar
INNER JOIN dbo.SomeSourceTable src ON ar.NoteID = src.NoteID
WHERE ar.UsrExternalRef IS NULL;
SELECT COUNT(*) FROM dbo.FSSrvOrdType WHERE UsrExternalRef IS NOT NULL;

For production deployments, prefer the Import Scenario. The audit trail alone is worth the slight extra effort. See the data migration guide for the full pattern.

Upgrade safety

Customisations break on Acumatica upgrades most often because the field they added depended on a base DAC field that was renamed, removed, or had its type changed. The defensive habits:

Test on production data

The most common upgrade failure is "the field worked in test but the upgrade failed in production because the data shape was different". Test your customisation on a copy of the production database, not on a sample. The cost of the staging tenant is a fraction of the cost of a production failure.

For the full upgrade playbook, see the upgrade checklist and the customisation upgrade survival guide.

Wrapping up

Adding a custom field to the Acumatica Field Service is not a 5-minute job, but it is also not a 5-day job if you follow the right pattern. The DAC extension, the screen configuration, the graph event handler, the data migration, and the upgrade test are the five steps. Do them in order; do them with attention; the result is a customisation that survives the next three Acumatica releases without intervention.

If your team is adding custom fields at scale (more than 20 across multiple modules), consider a naming convention document and a shared snippet library. Both pay for themselves within a quarter.