Process-Stage Transition Maps
Workflow stage validation within order processes - enforce valid transitions, prevent invalid stage jumps, and maintain operational integrity.
Process-Stage Validation
Comprehensive workflow stage management:
- Process-Specific Stages - Each process has its own valid stage set
- Transition Rules - Define allowed stage-to-stage movements
- Invalid Transition Prevention - Stop illegal stage jumps before they happen
- Stage Flow Enforcement - Maintain correct operational sequences
- Pipeline Integration - Validates automated and manual transitions
- Validation Helpers - Reusable functions for consistent enforcement
Use Cases: Prevent skipping steps, enforce sequential workflows, validate pipeline transitions, guide operators
What are Process-Stage Maps?
In logistics operations, orders move through specific processes (like FULFILLMENT or RECEIVING), and within each process, they transition through stages (like ALLOCATION → PICKING → PACKING). Process-stage maps define which stages are valid for each process and which transitions are allowed.
Two-Layer Validation Architecture
De. separates concerns between orchestration (pipelines) and business logic (order system):
Layer 1: Pipeline Stage Orchestration
// Pipeline templates define workflow steps
Pipeline: "Outbound Fulfillment"
Stages:
1. Inventory Allocation
2. Picking
3. Packing
4. Labeling
5. Shipping HandoffLayer 2: Order Process-Stage Validation (This layer)
// Business rules enforce valid order state transitions
Process: "FULFILLMENT"
Valid Stages: [ALLOCATION, PICKING, PACKING, SHIPPING]
Transitions:
ALLOCATION → PICKING ✅
PICKING → PACKING ✅
PACKING → SHIPPING ✅
PICKING → SHIPPING ❌ (cannot skip packing)Why Separate?
- Pipelines orchestrate what happens (workflow automation)
- Process-stage maps enforce what's valid (business rules)
- Pipelines can change without breaking business logic
- Manual operations use same validation as pipelines
The Problem Without Validation
Scenario: E-commerce Fulfillment
// ❌ Without validation - manual route
await db.updateOne(
{ reference: 'OUT-001' },
{ $set: { 'flow.stage': 'SHIPPING' } }
)
// Problems:
// - Skipped PICKING stage (items not picked yet!)
// - Skipped PACKING stage (nothing to ship!)
// - Customer gets shipping notification but order still in warehouse
// - No way to detect this was invalidWith Validation:
// ✅ With validation
const validation = isValidStageTransition(
'OutboundOrder',
'FULFILLMENT',
'ALLOCATION', // Current stage
'SHIPPING' // Trying to skip to shipping
)
// Returns:
{
valid: false,
reason: "Cannot transition from ALLOCATION to SHIPPING",
validNextStages: ['PICKING']
}
// Prevents invalid operation before it happensProcess-Stage Map Schema
Map Structure
Each order type has process-stage maps:
{
orderType: 'OutboundOrder',
processes: {
'FULFILLMENT': {
process: 'FULFILLMENT',
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING', 'DELIVERY'],
stageFlow: [
{ from: 'ALLOCATION', to: ['PICKING'] },
{ from: 'PICKING', to: ['PACKING'] },
{ from: 'PACKING', to: ['SHIPPING'] },
{ from: 'SHIPPING', to: ['DELIVERY'] },
{ from: 'DELIVERY', to: [] } // Terminal stage
]
}
}
}Key Components:
process- The business process nameallowedStages- Complete list of valid stages for this processstageFlow- Stage-to-stage transition rulesfrom- Current stageto- Array of allowed next stages (empty = terminal)
Built-in Process-Stage Maps
OutboundOrder
E-commerce and B2B fulfillment operations:
FULFILLMENT Process
{
process: 'FULFILLMENT',
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING', 'DELIVERY'],
stageFlow: [
{ from: 'ALLOCATION', to: ['PICKING'] },
{ from: 'PICKING', to: ['PACKING'] },
{ from: 'PACKING', to: ['SHIPPING'] },
{ from: 'SHIPPING', to: ['DELIVERY'] },
{ from: 'DELIVERY', to: [] }
]
}Workflow:
ALLOCATION (Reserve inventory)
↓
PICKING (Collect items from shelves)
↓
PACKING (Pack into boxes)
↓
SHIPPING (Hand off to carrier)
↓
DELIVERY (Final delivery to customer)Rules:
- ✅ Must follow sequential flow
- ❌ Cannot skip stages
- ❌ Cannot go backwards (no undo)
- ✅ Terminal at DELIVERY
InboundOrder
Receiving and put-away operations:
RECEIVING Process
{
process: 'RECEIVING',
allowedStages: ['ARRIVAL', 'UNLOADING', 'INSPECTION', 'PUT_AWAY'],
stageFlow: [
{ from: 'ARRIVAL', to: ['UNLOADING'] },
{ from: 'UNLOADING', to: ['INSPECTION'] },
{ from: 'INSPECTION', to: ['PUT_AWAY'] },
{ from: 'PUT_AWAY', to: [] }
]
}QUALITY_CHECK Process
{
process: 'QUALITY_CHECK',
allowedStages: ['INSPECTION', 'TESTING', 'APPROVAL', 'REJECTION'],
stageFlow: [
{ from: 'INSPECTION', to: ['TESTING'] },
{ from: 'TESTING', to: ['APPROVAL', 'REJECTION'] }, // Can branch
{ from: 'APPROVAL', to: [] },
{ from: 'REJECTION', to: [] }
]
}PUT_AWAY Process
{
process: 'PUT_AWAY',
allowedStages: ['STAGING', 'MOVING', 'STORING'],
stageFlow: [
{ from: 'STAGING', to: ['MOVING'] },
{ from: 'MOVING', to: ['STORING'] },
{ from: 'STORING', to: [] }
]
}CROSS_DOCK Process
{
process: 'CROSS_DOCK',
allowedStages: ['RECEIVING', 'SORTING', 'LOADING'],
stageFlow: [
{ from: 'RECEIVING', to: ['SORTING'] },
{ from: 'SORTING', to: ['LOADING'] },
{ from: 'LOADING', to: [] }
]
}InternalOrder
Warehouse task processes:
PICKING Process
{
process: 'PICKING',
allowedStages: ['ASSIGNED', 'TRAVELING', 'PICKING', 'VERIFICATION'],
stageFlow: [
{ from: 'ASSIGNED', to: ['TRAVELING'] },
{ from: 'TRAVELING', to: ['PICKING'] },
{ from: 'PICKING', to: ['VERIFICATION'] },
{ from: 'VERIFICATION', to: [] }
]
}PACKING Process
{
process: 'PACKING',
allowedStages: ['PREPARATION', 'PACKING', 'SEALING', 'LABELING'],
stageFlow: [
{ from: 'PREPARATION', to: ['PACKING'] },
{ from: 'PACKING', to: ['SEALING'] },
{ from: 'SEALING', to: ['LABELING'] },
{ from: 'LABELING', to: [] }
]
}CYCLE_COUNT Process
{
process: 'CYCLE_COUNT',
allowedStages: ['ASSIGNED', 'COUNTING', 'VERIFICATION', 'RECONCILIATION'],
stageFlow: [
{ from: 'ASSIGNED', to: ['COUNTING'] },
{ from: 'COUNTING', to: ['VERIFICATION'] },
{ from: 'VERIFICATION', to: ['RECONCILIATION'] },
{ from: 'RECONCILIATION', to: [] }
]
}REPLENISHMENT Process
{
process: 'REPLENISHMENT',
allowedStages: ['TRIGGERED', 'PICKING', 'MOVING', 'RESTOCKING'],
stageFlow: [
{ from: 'TRIGGERED', to: ['PICKING'] },
{ from: 'PICKING', to: ['MOVING'] },
{ from: 'MOVING', to: ['RESTOCKING'] },
{ from: 'RESTOCKING', to: [] }
]
}RELOCATION Process
{
process: 'RELOCATION',
allowedStages: ['ASSIGNED', 'PICKING', 'MOVING', 'PLACING'],
stageFlow: [
{ from: 'ASSIGNED', to: ['PICKING'] },
{ from: 'PICKING', to: ['MOVING'] },
{ from: 'MOVING', to: ['PLACING'] },
{ from: 'PLACING', to: [] }
]
}KITTING Process
{
process: 'KITTING',
allowedStages: ['PREPARATION', 'ASSEMBLY', 'VERIFICATION', 'PACKAGING'],
stageFlow: [
{ from: 'PREPARATION', to: ['ASSEMBLY'] },
{ from: 'ASSEMBLY', to: ['VERIFICATION'] },
{ from: 'VERIFICATION', to: ['PACKAGING'] },
{ from: 'PACKAGING', to: [] }
]
}ASSEMBLY Process
{
process: 'ASSEMBLY',
allowedStages: ['PREPARATION', 'ASSEMBLING', 'TESTING', 'PACKAGING'],
stageFlow: [
{ from: 'PREPARATION', to: ['ASSEMBLING'] },
{ from: 'ASSEMBLING', to: ['TESTING'] },
{ from: 'TESTING', to: ['PACKAGING'] },
{ from: 'PACKAGING', to: [] }
]
}ShippingOrder
Last-mile delivery operations:
DELIVERY Process
{
process: 'DELIVERY',
allowedStages: ['DISPATCHED', 'IN_TRANSIT', 'ARRIVED', 'DELIVERED'],
stageFlow: [
{ from: 'DISPATCHED', to: ['IN_TRANSIT'] },
{ from: 'IN_TRANSIT', to: ['ARRIVED'] },
{ from: 'ARRIVED', to: ['DELIVERED'] },
{ from: 'DELIVERED', to: [] }
]
}PICKUP Process
{
process: 'PICKUP',
allowedStages: ['SCHEDULED', 'DISPATCHED', 'ARRIVED', 'PICKED_UP'],
stageFlow: [
{ from: 'SCHEDULED', to: ['DISPATCHED'] },
{ from: 'DISPATCHED', to: ['ARRIVED'] },
{ from: 'ARRIVED', to: ['PICKED_UP'] },
{ from: 'PICKED_UP', to: [] }
]
}RETURN Process
{
process: 'RETURN',
allowedStages: ['INITIATED', 'PICKED_UP', 'IN_TRANSIT', 'RETURNED'],
stageFlow: [
{ from: 'INITIATED', to: ['PICKED_UP'] },
{ from: 'PICKED_UP', to: ['IN_TRANSIT'] },
{ from: 'IN_TRANSIT', to: ['RETURNED'] },
{ from: 'RETURNED', to: [] }
]
}PassthroughOrder
Cross-dock hub operations:
CROSS_DOCK Process
{
process: 'CROSS_DOCK',
allowedStages: ['RECEIVING', 'SORTING', 'ROUTING'],
stageFlow: [
{ from: 'RECEIVING', to: ['SORTING'] },
{ from: 'SORTING', to: ['ROUTING'] },
{ from: 'ROUTING', to: [] }
]
}SortingOrder
Hub sortation operations:
SORTING Process
{
process: 'SORTING',
allowedStages: ['RECEIVING', 'SCANNING', 'SORTING', 'DISPATCHING'],
stageFlow: [
{ from: 'RECEIVING', to: ['SCANNING'] },
{ from: 'SCANNING', to: ['SORTING'] },
{ from: 'SORTING', to: ['DISPATCHING'] },
{ from: 'DISPATCHING', to: [] }
]
}Validation Functions
isValidStageForProcess
Check if a stage is valid for a given process:
import { isValidStageForProcess } from '#lib/order/helpers'
const validation = isValidStageForProcess(
'OutboundOrder',
'FULFILLMENT',
'PICKING'
)
// Returns:
{
valid: true
}
// Or if invalid:
{
valid: false,
reason: "Stage 'RECEIVING' is not allowed for process 'FULFILLMENT'",
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING', 'DELIVERY']
}Use Case: Validate pipeline template configuration
// Pipeline template specifies stage
const stageTemplate = {
name: 'Picking Stage',
nextStage: 'PICKING', // Target order stage
orderType: 'OutboundOrder',
orderProcess: 'FULFILLMENT'
}
// Validate template is correct
const validation = isValidStageForProcess(
stageTemplate.orderType,
stageTemplate.orderProcess,
stageTemplate.nextStage
)
if (!validation.valid) {
throw new Error(`Invalid pipeline template: ${validation.reason}`)
}isValidStageTransition
Check if a stage transition is allowed:
import { isValidStageTransition } from '#lib/order/helpers'
const validation = isValidStageTransition(
'OutboundOrder',
'FULFILLMENT',
'PICKING', // Current stage
'PACKING' // Next stage
)
// Returns:
{
valid: true
}
// Or if invalid:
{
valid: false,
reason: "Cannot transition from 'PICKING' to 'SHIPPING'",
validNextStages: ['PACKING']
}Use Case: Validate manual stage updates
// Manual stage update route
.patch('/:reference/stage', async (req, rep) => {
const order = await db.findOne({ reference })
const { newStage } = req.body
const validation = isValidStageTransition(
'OutboundOrder',
order.flow.process,
order.flow.stage, // Current: PICKING
newStage // Requested: SHIPPING
)
if (!validation.valid) {
return rep.status(400).send({
error: true,
message: validation.reason,
currentStage: order.flow.stage,
requestedStage: newStage,
validNextStages: validation.validNextStages
})
}
// Safe to update
await db.updateOne({ reference }, { $set: { 'flow.stage': newStage } })
})getValidNextStages
Get all allowed next stages from current stage:
import { getValidNextStages } from '#lib/order/helpers'
const nextStages = getValidNextStages(
'OutboundOrder',
'FULFILLMENT',
'PICKING'
)
// Returns: ['PACKING']Use Case: UI guidance for operators
// Show operators which stages they can move to
const currentStage = order.flow.stage
const nextOptions = getValidNextStages(orderType, process, currentStage)
// Display dropdown:
// "Move to:" [PACKING] ← Only valid option showngetAllowedStagesForProcess
Get complete list of stages for a process:
import { getAllowedStagesForProcess } from '#lib/order/helpers'
const stages = getAllowedStagesForProcess(
'OutboundOrder',
'FULFILLMENT'
)
// Returns: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING', 'DELIVERY']Use Case: Pipeline template validation
// Validate all pipeline stages are valid for the process
const pipelineStages = ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']
const allowedStages = getAllowedStagesForProcess('OutboundOrder', 'FULFILLMENT')
const invalidStages = pipelineStages.filter(s => !allowedStages.includes(s))
if (invalidStages.length > 0) {
throw new Error(`Invalid stages: ${invalidStages.join(', ')}`)
}isTerminalStage
Check if a stage is terminal (no outgoing transitions):
import { isTerminalStage } from '#lib/order/helpers'
const isTerminal = isTerminalStage(
'OutboundOrder',
'FULFILLMENT',
'DELIVERY'
)
// Returns: true (DELIVERY has no next stages)
const isTerminal2 = isTerminalStage(
'OutboundOrder',
'FULFILLMENT',
'PICKING'
)
// Returns: false (PICKING → PACKING)Use Case: Auto-complete orders at terminal stage
// When reaching terminal stage, auto-complete order
const isTerminal = isTerminalStage(orderType, process, newStage)
if (isTerminal) {
// No more stages → complete order
await db.updateOne(
{ reference },
{ $set: { 'flow.status': 'COMPLETED' } }
)
}Pipeline Integration
Pipeline Stage Templates
Pipelines define stages that map to order stages:
// Pipeline Template
{
id: 'pipe-outbound-v1',
name: 'Standard Outbound Fulfillment',
stages: [
{
name: 'Inventory Allocation',
nextStage: 'ALLOCATION', // Maps to order stage
orderType: 'OutboundOrder',
orderProcess: 'FULFILLMENT'
},
{
name: 'Picking',
nextStage: 'PICKING',
orderType: 'OutboundOrder',
orderProcess: 'FULFILLMENT'
},
{
name: 'Packing',
nextStage: 'PACKING',
orderType: 'OutboundOrder',
orderProcess: 'FULFILLMENT'
}
]
}Validation on Pipeline Transitions
When pipeline transitions stages, validation runs:
// In transitions.ts - handleSuccessTransition
case 'CONTINUE':
if (successAction.nextStage) {
// ✅ Validate stage transition
const validation = isValidStageTransition(
execution.orderType,
currentOrder.flow.process,
currentOrder.flow.stage,
successAction.nextStage
)
if (!validation.valid) {
// ❌ Invalid transition - fail pipeline
execution.stages[currentStageIndex].status = 'FAILED'
execution.stages[currentStageIndex].errors.push({
code: 'INVALID_STAGE_TRANSITION',
message: validation.reason,
timestamp: Date.now()
})
return
}
// ✅ Valid - update order stage
await updateOrder(orderId, {
'flow.stage': successAction.nextStage
})
}
breakResult: Pipelines cannot create invalid stage transitions - same rules as manual operations.
Common Patterns
Sequential Flow
Most processes use strict sequential flow:
stageFlow: [
{ from: 'STAGE_1', to: ['STAGE_2'] },
{ from: 'STAGE_2', to: ['STAGE_3'] },
{ from: 'STAGE_3', to: [] }
]
// STAGE_1 → STAGE_2 → STAGE_3 (linear)Branching Flow
Some processes allow branching:
stageFlow: [
{ from: 'INSPECTION', to: ['TESTING'] },
{ from: 'TESTING', to: ['APPROVAL', 'REJECTION'] }, // Branch
{ from: 'APPROVAL', to: [] },
{ from: 'REJECTION', to: [] }
]
// ┌→ APPROVAL
// INSPECTION → TESTING
// └→ REJECTIONParallel Paths
Multiple valid next stages (rare):
stageFlow: [
{ from: 'RECEIVING', to: ['PUT_AWAY', 'CROSS_DOCK', 'QUARANTINE'] },
{ from: 'PUT_AWAY', to: [] },
{ from: 'CROSS_DOCK', to: [] },
{ from: 'QUARANTINE', to: [] }
]
// ┌→ PUT_AWAY
// RECEIVING ┼→ CROSS_DOCK
// └→ QUARANTINETerminal Stages
Stages with no outgoing transitions:
stageFlow: [
{ from: 'DELIVERY', to: [] } // Terminal - nowhere to go
]Best Practices
Design Linear Workflows First
✅ Start Simple:
stageFlow: [
{ from: 'A', to: ['B'] },
{ from: 'B', to: ['C'] },
{ from: 'C', to: [] }
]
// Easy to understand, maintain, and validate❌ Avoid Complexity:
stageFlow: [
{ from: 'A', to: ['B', 'C', 'D'] },
{ from: 'B', to: ['C', 'D', 'E'] },
{ from: 'C', to: ['B', 'D', 'E', 'F'] }
]
// Confusing, hard to validate, error-proneUse Meaningful Stage Names
✅ Descriptive:
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']
// Clear what happens at each stage❌ Generic:
allowedStages: ['STEP_1', 'STEP_2', 'STEP_3', 'STEP_4']
// What happens at STEP_2?Match Process to Operations
✅ Process-Aligned:
// FULFILLMENT process
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']
// All stages relate to fulfillment❌ Mixed Concerns:
// FULFILLMENT process
allowedStages: ['ALLOCATION', 'PICKING', 'QUALITY_CHECK', 'BILLING', 'SHIPPING']
// QUALITY_CHECK and BILLING don't belong in FULFILLMENTValidate Early
✅ Validate at Configuration:
// When creating pipeline template
const validation = isValidStageForProcess(orderType, process, stage)
if (!validation.valid) {
throw new Error('Invalid pipeline configuration')
}✅ Validate at Runtime:
// When transitioning stages
const validation = isValidStageTransition(type, process, from, to)
if (!validation.valid) {
return { error: true, reason: validation.reason }
}Use Cases
E-Commerce Fulfillment
Sequential Workflow:
ALLOCATION → PICKING → PACKING → SHIPPING → DELIVERYValidation Prevents:
- Skipping picking (ship without collecting items)
- Skipping packing (ship loose items)
- Going backwards (unpacking after shipping)
- Invalid stages (billing stage in fulfillment process)
Quality Control
Branching Workflow:
INSPECTION → TESTING → APPROVAL or REJECTIONValidation Prevents:
- Approving without testing
- Rejecting without inspection
- Moving from approval to rejection (final states)
Cross-Dock Operations
Fast Turnaround:
RECEIVING → SORTING → ROUTINGValidation Prevents:
- Routing before sorting
- Skipping critical sortation step
- Going backwards (re-receiving)
Troubleshooting
Invalid Transition Errors
Error: "Cannot transition from PICKING to SHIPPING"
Cause: Trying to skip PACKING stage
Fix:
// ❌ Invalid
await updateStage('PICKING', 'SHIPPING')
// ✅ Valid
await updateStage('PICKING', 'PACKING')
await updateStage('PACKING', 'SHIPPING')Stage Not Allowed for Process
Error: "Stage 'RECEIVING' is not allowed for process 'FULFILLMENT'"
Cause: Wrong process for the operation
Fix:
// ❌ Invalid
order.flow = { process: 'FULFILLMENT', stage: 'RECEIVING' }
// ✅ Valid
order.flow = { process: 'RECEIVING', stage: 'ARRIVAL' }
// or
order.flow = { process: 'FULFILLMENT', stage: 'ALLOCATION' }No Transitions Defined
Error: "No transitions defined from stage 'CUSTOM_STAGE'"
Cause: Using undefined stage
Fix:
// Check allowed stages
const allowed = getAllowedStagesForProcess(orderType, process)
console.log(allowed) // ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']
// Use valid stage
order.flow.stage = 'PICKING' // ✅ In allowed listNext Steps
Related Documentation:

