Workflow · Workflow

Acumatica Workflow — Approval by Amount Thresholds

Acumatica Workflow — Approval by Amount Thresholds is one of the most common workflow customisations in Acumatica.

John Kihiu12 min read

Amount-based approval is the single most common workflow request I get, and also the one people most reliably get wrong on the first attempt — not because the Workflow engine can't express it, but because the amount that should gate approval is rarely the field people first reach for. This post is the pattern I actually use, with the currency and rounding traps that show up once real transactions start flowing.

Pick the right amount field first

Every AP/AR/SO document carries several amount-shaped fields, and they are not interchangeable for approval logic: CuryDocBal (outstanding balance, moves as payments apply — wrong for approval, since an already-approved invoice would re-trigger as it gets paid down), CuryOrigDocAmt (the original document total in document currency — usually right), and DocTotal or OrderTotal in the base/company currency (right when you need one threshold across multi-currency documents, since a KES 500,000 order and a USD 5,000 order should probably hit the same approval bar in a multi-branch, multi-currency tenant).

C#
public static void Configure(WorkflowContext<APInvoiceEntry, APInvoice> context)
{
    var graph = context.Graph;
    graph.WithTransitions(transitions => transitions
        .AddGroupFrom<APInvoice.status.Balanced>(g => g
            .Add(t => t
                .To<APInvoice.status.PendingApproval>()
                .IsTriggeredOn(a => a.Actions.Release)
                // base-currency total, NOT CuryDocBal
                .When(APInvoice.docTotal.IsGreaterEqual(500000m)))
            .Add(t => t
                .To<APInvoice.status.Open>()
                .IsTriggeredOn(a => a.Actions.Release)
                .When(APInvoice.docTotal.IsLess(500000m)))));
}

Tiered thresholds, not a single cutoff

"Approval above $5,000" is the requirement as first stated and almost never the requirement as actually needed — real approval matrices are tiered: a supervisor approves up to $5,000, a controller up to $25,000, the CFO above that. Model this as a chain of transitions into different pending-approval states, each carrying its own approval map assignment, rather than one state with a single approver list and a mental "well actually check the amount again" step for the human approving it:

C#
.Add(t => t.To<APInvoice.status.PendingApprovalTier1>()
           .IsTriggeredOn(a => a.Actions.Release)
           .When(APInvoice.docTotal.IsGreaterEqual(5000m)
                 & APInvoice.docTotal.IsLess(25000m)))
.Add(t => t.To<APInvoice.status.PendingApprovalTier2>()
           .IsTriggeredOn(a => a.Actions.Release)
           .When(APInvoice.docTotal.IsGreaterEqual(25000m)))

Each tier state then gets its own row on the Assignment/Approval Maps screen (EP503010) with a distinct approver — a workgroup, a role, or a rule keyed off the vendor's owner. Keeping the states distinct also means your reporting on "what's stuck in tier-2 approval" is a plain status filter instead of a computed field.

Boundary rounding: decide it explicitly

Is an invoice for exactly $5,000.00 tier 1 or tier 2? IsGreaterEqual versus IsGreater on the boundary condition is a business decision, not a technical one — write it down in the requirements doc and pick consistently, because I have seen two adjacent tier conditions both use strict IsGreater at the same boundary, leaving a document at exactly the threshold matching neither transition and getting stuck with no eligible outbound transition at all. That failure mode is silent: the document just sits there with a Release button that does nothing, because no transition's condition matched.

Gaps in tiered conditions strand documents

Whenever you build adjacent numeric tiers, verify the boundaries are complementary — one tier's upper bound should be exactly the next tier's lower bound, using consistent >= / < pairing. Test the exact boundary value, not just values comfortably inside each tier; that's where gaps hide.

Wiring the approval map itself

The transition gets you into a pending-approval state; the Assignment and Approval Maps screen decides who has to act. A map row references the graph, the specific status, and an assignment rule — by employee, by role, or (the one clients actually want) by a condition on the document itself, such as "the vendor's assigned buyer" resolved through a relation to EPEmployee. Multiple approvers on one map row can be sequential or require-all; that's a parallel-approval concern I cover separately, but for a straight amount ladder, one approver per tier is usually the whole requirement.

Wrapping up

Amount-based approval done well is three decisions made deliberately: which amount field (base currency total, not a balance that moves), how many tiers (distinct states, not one state with fuzzy logic), and boundary inclusivity (verified with an exact-value test, not eyeballed). Get those three right and the approval map configuration on EP503010 is almost mechanical.

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.