Purchase Order Routing
IT, Operations, and Marketing each have their own approval chain. Enterprise software requires Security sign-off. Finance steps in above $10k. Split PO detection catches attempts to circumvent approval thresholds.
engine.registerResolver('direct-manager', (submittedBy) => hrSystem.managerIdOf(submittedBy));
const department = (name: string) => ({ field: 'department', operator: '==', value: name });
await engine.defineTemplate({
name: 'purchase-order',
documentType: 'purchase-order',
allowOverride: true, // required for engine.override() below
levels: [
{
level: 1,
name: 'Manager',
approvers: [{ type: 'dynamic', resolver: 'direct-manager' }],
mode: 'any',
},
],
conditions: [
{
when: department('it'),
addLevels: [{ level: 2, name: 'IT director', approvers: [{ type: 'role', role: 'it-director' }], mode: 'any' }],
},
// Enterprise software also needs Security sign-off
{
when: { all: [department('it'), { field: 'isEnterpriseContract', operator: '==', value: true }] },
addLevels: [{ level: 3, name: 'Security', approvers: [{ type: 'role', role: 'security-officer' }], mode: 'any' }],
},
{
when: { all: [department('marketing'), { field: 'amount', operator: '>', value: 5_000 }] },
addLevels: [{ level: 2, name: 'Marketing director', approvers: [{ type: 'role', role: 'marketing-director' }], mode: 'any' }],
},
{
when: department('operations'),
addLevels: [{ level: 2, name: 'Operations head', approvers: [{ type: 'role', role: 'operations-head' }], mode: 'any' }],
},
// Finance is a universal gate above $10,000
{
when: { field: 'amount', operator: '>', value: 10_000 },
addLevels: [{ level: 4, name: 'Finance', approvers: [{ type: 'role', role: 'finance-controller' }], mode: 'any' }],
},
],
}); Three scenarios that break hand-rolled PO systems.
Each one involves state coordination that a simple if/else can't handle safely.
Split PO detection
An employee submits a $9,999 PO on Monday and a $8,000 PO to the same vendor on Wednesday — both under the $10k Finance threshold. Combined: $17,999.
// Sum the vendor's open POs from the same requester over the last 30 days
async function openTotalForVendor(vendorId: string, submittedBy: string): Promise<number> {
const { items } = await engine.queryInstances(
{
documentType: 'purchase-order',
status: 'pending',
submittedBy,
fromDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
data: { vendorId },
},
{ limit: 500, offset: 0 }
);
return items.reduce((total, po) => total + Number(po.data.amount), 0);
}
// Call before engine.submit()
const combined = (await openTotalForVendor(vendorId, userId)) + amount;
if (amount <= 10_000 && combined > 10_000) {
throw new Error(`Split PO detected: combined total ${combined}`);
}
Run the check in your API before calling engine.submit().
queryInstances matches data fields by equality, so
the amount is summed in your code.
Emergency procurement override
A production server fails at 2am. IT needs to order a replacement immediately. The normal 48-hour approval cycle isn't viable.
// Emergency procurement: bypass the remaining levels
const instance = await engine.override(poInstance.id, {
overriddenBy: 'admin-cso',
justification: 'Emergency server replacement - P0 incident, board notified',
});
// instance.status === 'approved'
// The audit trail gets an 'overridden' entry:
// { action: 'overridden', actorId: 'admin-cso', reason: 'Emergency server replacement ...', ... } override() only works on templates with allowOverride: true.
Restrict who may call it with an authorizationPolicy (the operation is 'override').
The justification is stored on the audit entry.
Partial line-item rejection
Finance approves the hardware line but rejects the 3-year extended warranty. Approval is per document, so the requester revises the PO and resubmits it.
await engine.reject(instance.id, {
approverId: 'finance-fiona',
reason: 'Hardware approved; remove the 3-year extended warranty line',
});
// The requester fixes the data and resubmits
const revised = await engine.resubmit(instance.id, {
resubmittedBy: 'emp-0712',
reason: 'Warranty line removed',
updatedData: { amount: 7_000 },
});
// revised.status === 'pending', revised.parentInstanceId === instance.id
The rejected instance stays in the audit trail as status: rejected.
resubmit() creates a new instance from level 1 and links it
through parentInstanceId.
Your next approval workflow
ships faster than the last one.
Drop it into any Node.js project. No database required for local development.
$ npm install hierarchical-approval Peer dep for Postgres: npm install pg @types/pg