Every Acumatica screen that has a status dropdown and a row of conditional action buttons is running on the Workflow engine, whether or not anyone building customizations on top of it realizes it. I get asked to explain "the workflow thing" often enough — usually by a developer who learned Acumatica on an older build and is confused why PXAction visibility used to live in C# and now lives in XML — that it is worth laying out the whole shape of the system once, properly, rather than re-explaining fragments of it in every project.
What the Workflow engine actually replaced
Before roughly 2019 R1, screen behavior — which buttons show, which fields are read-only, what happens when a document moves from Open to Closed — was hand-coded: PXUIFieldAttribute.SetEnabled calls sprinkled through RowSelected handlers, action visibility toggled in Initialize, and state transitions modeled as a bespoke pattern people called "the state machine" (you'll still find acumatica-state-machine-in-graph-extensions-style code on older customizations). It worked, but every screen reinvented its own dialect, and none of it was visible anywhere except by reading code.
The Workflow engine centralizes that into a declarative graph attached to a PXGraph: a set of named states, transitions between them, conditions that gate which transitions are legal, and actions whose availability and behavior are computed from the current state. Crucially, this graph is inspectable and editable from the UI — the Workflow screen — not just from code, which is why business analysts can now tweak an approval threshold without a deployment.
The shape of a workflow definition
Every workflow-enabled graph configures itself through a static Configure method taking a WorkflowContext<TGraph>. The four building blocks you compose are always the same:
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public static void Configure(WorkflowContext<SOOrderEntry, SOOrder> context)
{
var graph = context.Graph;
graph.WithStates(states =>
{
states.Add<SOOrder.status.Open>(state => state
.Menu(m => m.WithCategory("Status"))
.UpdateSettingsFrom(context.Graph.FirstIn<SOOrder.status.Open>()));
states.Add<SOOrder.status.PendingApproval>(state => state
.IsSpecific()
.Menu(m => m.DisplayName("Pending Approval")));
});
graph.WithActions(actions =>
{
actions.Add(g => g.Actions.Save,
a => a.PlaceAfter(g => g.Actions.Insert));
});
graph.WithTransitions(transitions => transitions
.AddGroupFrom<SOOrder.status.Open>(g => g
.Add(t => t
.To<SOOrder.status.PendingApproval>()
.IsTriggeredOn(g2 => g2.Actions.Save)
.When(SOOrder.orderTotal.IsGreaterEqual(5000m))
.WithFieldStates(fs => fs.Set<SOOrder.customerID>(f => f.Disabled()))
)));
});
}
That is the whole vocabulary: WithStates declares the finite set of statuses a document can be in; WithActions repositions or configures the toolbar buttons the graph already exposes; WithTransitions is where the real logic lives — which state leads to which other state, what triggers the move, what conditions gate it, and what the UI should look like once you're there (via WithFieldStates).
Conditions are BQL, not arbitrary C#
The .When(...) clause on a transition takes a WorkflowBqlExpression, built from the same field-comparison vocabulary as BQL Where clauses — IsEqual, IsGreaterEqual, IsNotNull, combined with & and |. This matters because it means conditions are evaluated declaratively against the cached row, not by running arbitrary code — which is exactly why the Workflow screen can show you, in a human-readable diagram, which conditions gate which transitions without executing anything. The trade-off: if your condition genuinely needs a database round trip or a call into another graph, it does not belong in .When() — do that work in a category handler or category-based action delegate and drive a flag field the condition can read instead.
A workflow "category" is not always a stored field. Acumatica lets you condition on virtual, unbound fields computed at runtime (useful for cross-cutting rules like "is this a related-party transaction"), but most production workflows key transitions off a real persisted status field so history and reporting stay coherent after the workflow definition itself changes.
Screen Editor versus code: two ways in, one model underneath
You can build or modify a workflow two ways: through the Workflow screen (a visual designer that generates the same Configure-shaped definition behind the scenes and stores it against the screen ID) or by writing Configure directly in a PXGraphExtension. In practice I do both on the same project — draft the state diagram visually with a client in the room so they can point at boxes, then move anything with real logic (custom conditions referencing extension fields, multi-branch transitions) into code where it can be source-controlled and code-reviewed. Screen-designed workflows technically live in the database as configuration records, which is friendly for citizen customizers but awkward for change management; code-based workflows travel in your customization project like everything else.
Where the rest of this series goes
The posts linked below each dig into one corner of this system with real screen IDs and real gotchas: amount-based approval maps, parallel approver graphs, converting a template workflow into something screen-specific, and the sharp edges of state/condition modeling when two workflows on related graphs need to stay in sync. This post is the map; those are the terrain.
Wrapping up
The Workflow engine is not an approval-routing bolt-on — it is the mechanism behind every status pill and disabled field you have ever seen in Acumatica, whether the screen is stock or yours. Once you can read a Configure method fluently — states, actions, transitions, conditions, field states — you can predict how any screen will behave under a given data state without stepping through a debugger, which is the whole point of making it declarative in the first place.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.