Acumatica · Customization

Acumatica Note Handler — Programmatic Patterns

The Notes tab on any Acumatica screen looks like a simple free-text log, and for a user it is. From code, generating or reading notes programmatically - auto-logging an approval.

John Kihiu12 min read

The Notes tab on any Acumatica screen looks like a simple free-text log, and for a user it is. From code, generating or reading notes programmatically - auto-logging an approval decision, appending a system-generated comment when an integration syncs a record - goes through a small, specific API that's easy to get subtly wrong the first time, mostly around when a note is genuinely new versus when you're supposed to be appending to existing note text.

Notes and attachments are the same underlying mechanism, different content type

Same as file attachments, notes hang off a DAC's NoteID field into shared tables - in this case primarily Note, holding the actual text content, referenced by NoteID same as UploadFile is for attachments. A DAC without a NoteID field can't have programmatic notes any more than it can have attachments, for the same underlying reason: there's nowhere for the framework to hang the association.

C#
protected virtual void AppendSystemNote(SOOrder order, string text)
{
    if (order?.NoteID == null) return;

    var noteGraph = PXGraph.CreateInstance<PXGraph>();
    string existing = PXNoteAttribute.GetNote(Base.Caches[typeof(SOOrder)], order) ?? string.Empty;

    string combined = string.IsNullOrEmpty(existing)
        ? text
        : existing + Environment.NewLine + Environment.NewLine + text;

    PXNoteAttribute.SetNote(Base.Caches[typeof(SOOrder)], order, combined);
}

The most common bug: overwriting instead of appending

The naive version of "log a note when X happens" calls SetNote with just the new text, silently discarding whatever was there before - fine the first time, destructive the second time the same event fires on the same record. I've inherited a customization where every approval-stage transition wrote a fresh note that clobbered the entire audit trail of prior stages, because the developer never read the existing note text before calling SetNote. By the time anyone noticed, months of approval history across hundreds of orders were gone - there was no way to reconstruct it, because the old text had been genuinely overwritten, not archived anywhere.

If a note is meant to be an append-only log, treat GetNote-then-SetNote as one unit, every time

There's no built-in "append" method - append is a pattern you build yourself by reading the existing note, concatenating, and writing the combined text back. Skipping the read half of that pattern is indistinguishable from a working feature until the second time it runs against the same record, which is exactly why it survives code review and only surfaces as a problem in production, later, when it's much more expensive to fix.

When a note is the wrong tool for what you're actually building

Free-text notes are fine for human-readable commentary a user might scroll through, and genuinely wrong for anything you'll later need to query, filter, or report on structurally. "Log every price override with the old value, new value, user, and timestamp" sounds like a note at first glance, and is actually a case for a dedicated child DAC (an audit table with real typed columns) the moment anyone asks "show me all price overrides over $500 last month" - a query a free-text note field cannot answer without parsing strings, which is a bad place to end up. I ask "will this ever need to be queried, not just read" before reaching for a note versus a real audit table, and it's saved me from painting into this corner more than once.

Reading a note back, and the null-safety it actually needs

Reading is the mirror of writing, through PXNoteAttribute.GetNote, and needs the same defensive null-checking as any NoteID-based lookup - a row that's never had a note written to it has a null or empty result, not an exception, but code that assumes a note always exists (skipping the null check because "we always set one on creation") breaks the moment someone imports historical data through a path that never touched the note-writing code at all:

C#
string note = PXNoteAttribute.GetNote(Base.Caches[typeof(SOOrder)], order);
if (string.IsNullOrEmpty(note))
{
    // Genuinely no note yet - common on migrated or bulk-imported records
    return;
}

Wrapping up

Programmatic notes share NoteID plumbing with attachments, and the API is small - GetNote and SetNote - but SetNote replaces, it doesn't append, so any code building an append-only log needs to read the existing text first as a deliberate, non-optional step. Reach for a real child DAC instead of a note the moment the content needs to be queried or reported on structurally rather than just read by a human, and always null-guard GetNote against records that reached your code through a path that never wrote a note in the first place.

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.