Workflow · Workflow

Acumatica Workflow — Document Routing Patterns

Acumatica Workflow — Document Routing Patterns is one of the most common workflow customisations in Acumatica.

John Kihiu12 min read

Document routing is the workflow pattern that isn't about approval at all — it's about getting a document (most often a Case, on screen CR306000, graph CRCaseMaint) in front of the right team based on its content, without a human deciding first. I build this most often for support-case triage: route by product line, by severity, by customer tier, and let the workflow's transitions do what used to be a dispatcher's job.

Routing lives in transitions and owner assignment, not the approval map

This is the opposite emphasis from the approval-focused posts in this series: there's no approve/reject decision here, just "which state, and therefore which queue/owner, does this land in." The mechanism is still transitions and WithFieldStates, but the target states represent queues or teams rather than approval stages, and the "assignment" is a direct owner/workgroup set on the transition rather than a multi-step approval map.

C#
public static void Configure(WorkflowContext<CRCaseMaint, CRCase> context)
{
    var graph = context.Graph;
    graph.WithTransitions(transitions => transitions
        .AddGroupFrom<CRCase.caseStatus.New>(g => g
            .Add(t => t
                .To<CRCase.caseStatus.Open>()
                .IsTriggeredOn(a => a.Actions.Save)
                .When(CRCase.severity.IsEqual(CaseSeverity.Critical))
                .WithFieldStates(fs => fs
                    .Set<CRCase.workgroupID>(f => f.Default("TIER2-URGENT"))
                    .Set<CRCase.priority>(f => f.Disabled())))   // lock it, no downgrading
            .Add(t => t
                .To<CRCase.caseStatus.Open>()
                .IsTriggeredOn(a => a.Actions.Save)
                .When(CRCase.severity.IsNotEqual(CaseSeverity.Critical))
                .WithFieldStates(fs => fs
                    .Set<CRCase.workgroupID>(f => f.Default("TIER1-GENERAL"))))));
}

Content-based routing needs the same rollup pattern as discount approval

Routing by product line usually means inspecting a related record — the case's linked Item, or a custom classification field populated from a support portal's category picker — rather than a field natively on CRCase. As with the discount-approval rollup pattern, compute the routing key into an unbound or stored field the workflow condition can read directly; don't try to reach across the relation from inside .When(). For a support case, that's often as simple as defaulting a ProductLine field from the linked Item's item class at case creation, then conditioning transitions on that stable field rather than re-deriving it every time.

Escalation: routing that fires on a timer, not a user action

The routing requirement that goes beyond a simple Configure extension: "if a critical case sits unassigned for 30 minutes, escalate it automatically." Workflow transitions trigger on actions (Save, Release, a custom button) — they don't have a native timer trigger. I implement time-based escalation as an Automation Schedule running a small processing graph on a short interval, which finds cases past their SLA clock and calls the graph's action programmatically (invoking the same PXAction the transition is wired to, so the escalation goes through the identical workflow path a human clicking the button would trigger, keeping the audit trail consistent) rather than writing status directly to the database, which would bypass any field-state side effects the transition is supposed to apply.

Don't write status directly to bypass a stuck transition

It's tempting, when a routing transition isn't firing the way you expect, to fix the symptom by setting CRCase.CaseStatus directly in a scheduled processor. This skips every WithFieldStates side effect and any downstream automation keyed to the transition itself (notifications, SLA clock resets), and produces a case that's superficially in the right status but missing whatever the transition was supposed to also do. Always trigger the same action a user would click.

Routing changes visibility, which is its own testing burden

Because routing usually changes WorkgroupID/OwnerID, and Acumatica's row-level security for cases is commonly scoped to workgroup membership, a routing transition can make a case disappear from one team's queue and appear in another's — silently, from the reporter's point of view. Test routing changes logged in as a member of the losing team, not just the receiving one, to confirm the case actually vanishes from where it shouldn't be as well as appearing where it should.

Wrapping up

Document routing repurposes the same transition/condition/field-state vocabulary as approval workflows, but the target states represent destinations, not decisions — and the assignment is a direct field default, not a multi-step approval map. For time-based escalation, drive the same action a human would click from a scheduled processor rather than writing status directly, and always test routing from the perspective of the team losing visibility, not just the team gaining it.

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.