Skip to content

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

typescript
// Pipeline templates define workflow steps
Pipeline: "Outbound Fulfillment"
Stages:
  1. Inventory Allocation
  2. Picking
  3. Packing
  4. Labeling
  5. Shipping Handoff

Layer 2: Order Process-Stage Validation (This layer)

typescript
// Business rules enforce valid order state transitions
Process: "FULFILLMENT"
Valid Stages: [ALLOCATION, PICKING, PACKING, SHIPPING]
Transitions:
  ALLOCATIONPICKING
  PICKINGPACKING
  PACKINGSHIPPING
  PICKINGSHIPPING ❌ (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

typescript
// ❌ 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 invalid

With Validation:

typescript
// ✅ 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 happens

Process-Stage Map Schema

Map Structure

Each order type has process-stage maps:

typescript
{
  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 name
  • allowedStages - Complete list of valid stages for this process
  • stageFlow - Stage-to-stage transition rules
  • from - Current stage
  • to - Array of allowed next stages (empty = terminal)

Built-in Process-Stage Maps

OutboundOrder

E-commerce and B2B fulfillment operations:

FULFILLMENT Process

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  process: 'PUT_AWAY',
  allowedStages: ['STAGING', 'MOVING', 'STORING'],
  stageFlow: [
    { from: 'STAGING', to: ['MOVING'] },
    { from: 'MOVING', to: ['STORING'] },
    { from: 'STORING', to: [] }
  ]
}

CROSS_DOCK Process

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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

typescript
{
  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:

typescript
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

typescript
// 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:

typescript
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

typescript
// 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:

typescript
import { getValidNextStages } from '#lib/order/helpers'

const nextStages = getValidNextStages(
  'OutboundOrder',
  'FULFILLMENT',
  'PICKING'
)

// Returns: ['PACKING']

Use Case: UI guidance for operators

typescript
// 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 shown

getAllowedStagesForProcess

Get complete list of stages for a process:

typescript
import { getAllowedStagesForProcess } from '#lib/order/helpers'

const stages = getAllowedStagesForProcess(
  'OutboundOrder',
  'FULFILLMENT'
)

// Returns: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING', 'DELIVERY']

Use Case: Pipeline template validation

typescript
// 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):

typescript
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

typescript
// 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:

typescript
// 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:

typescript
// 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
    })
  }
  break

Result: Pipelines cannot create invalid stage transitions - same rules as manual operations.

Common Patterns

Sequential Flow

Most processes use strict sequential flow:

typescript
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:

typescript
stageFlow: [
  { from: 'INSPECTION', to: ['TESTING'] },
  { from: 'TESTING', to: ['APPROVAL', 'REJECTION'] },  // Branch
  { from: 'APPROVAL', to: [] },
  { from: 'REJECTION', to: [] }
]

//        ┌→ APPROVAL
// INSPECTION → TESTING
//        └→ REJECTION

Parallel Paths

Multiple valid next stages (rare):

typescript
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
//        └→ QUARANTINE

Terminal Stages

Stages with no outgoing transitions:

typescript
stageFlow: [
  { from: 'DELIVERY', to: [] }  // Terminal - nowhere to go
]

Best Practices

Design Linear Workflows First

✅ Start Simple:

typescript
stageFlow: [
  { from: 'A', to: ['B'] },
  { from: 'B', to: ['C'] },
  { from: 'C', to: [] }
]
// Easy to understand, maintain, and validate

❌ Avoid Complexity:

typescript
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-prone

Use Meaningful Stage Names

✅ Descriptive:

typescript
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']
// Clear what happens at each stage

❌ Generic:

typescript
allowedStages: ['STEP_1', 'STEP_2', 'STEP_3', 'STEP_4']
// What happens at STEP_2?

Match Process to Operations

✅ Process-Aligned:

typescript
// FULFILLMENT process
allowedStages: ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']
// All stages relate to fulfillment

❌ Mixed Concerns:

typescript
// FULFILLMENT process
allowedStages: ['ALLOCATION', 'PICKING', 'QUALITY_CHECK', 'BILLING', 'SHIPPING']
// QUALITY_CHECK and BILLING don't belong in FULFILLMENT

Validate Early

✅ Validate at Configuration:

typescript
// When creating pipeline template
const validation = isValidStageForProcess(orderType, process, stage)
if (!validation.valid) {
  throw new Error('Invalid pipeline configuration')
}

✅ Validate at Runtime:

typescript
// 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 → DELIVERY

Validation 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 REJECTION

Validation Prevents:

  • Approving without testing
  • Rejecting without inspection
  • Moving from approval to rejection (final states)

Cross-Dock Operations

Fast Turnaround:

RECEIVING → SORTING → ROUTING

Validation 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:

typescript
// ❌ 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:

typescript
// ❌ 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:

typescript
// Check allowed stages
const allowed = getAllowedStagesForProcess(orderType, process)
console.log(allowed)  // ['ALLOCATION', 'PICKING', 'PACKING', 'SHIPPING']

// Use valid stage
order.flow.stage = 'PICKING'  // ✅ In allowed list

Next Steps


Related Documentation: