Skip to content

State Workflows Example

This example builds a realistic order processing workflow using Specwright's StateMachine, showing transitions, hooks, history tracking, and combination with @spec.

The Order Lifecycle

stateDiagram-v2
    [*] --> pending
    pending --> paid: pay()
    paid --> shipped: ship()
    shipped --> delivered: deliver()
    pending --> cancelled: cancel()
    paid --> cancelled: cancel()

An order can follow the happy path (pending → paid → shipped → delivered) or be cancelled before shipping.

Implementation

from specwright import StateMachine, spec, transition


class OrderProcessor(StateMachine):
    states = ["pending", "paid", "shipped", "delivered", "cancelled"]
    initial_state = "pending"
    track_history = True

    def __init__(self, order_id: str) -> None:
        super().__init__()
        self.order_id = order_id

    @transition(from_state="pending", to_state="paid")
    @spec
    def pay(self, amount: float) -> str:
        """Process payment for the order."""
        return f"Order {self.order_id}: payment of ${amount:.2f} received"

    @transition(from_state="paid", to_state="shipped")
    @spec
    def ship(self, tracking: str) -> str:
        """Ship the order with a tracking number."""
        return f"Order {self.order_id}: shipped (tracking: {tracking})"

    @transition(from_state="shipped", to_state="delivered")
    @spec
    def deliver(self) -> str:
        """Mark the order as delivered."""
        return f"Order {self.order_id}: delivered"

    @transition(from_state=["pending", "paid"], to_state="cancelled")
    @spec
    def cancel(self, reason: str) -> str:
        """Cancel the order (only if not yet shipped)."""
        return f"Order {self.order_id}: cancelled ({reason})"

    # Lifecycle hooks
    def on_enter_paid(self) -> None:
        print(f"  [hook] Sending payment confirmation for {self.order_id}")

    def on_enter_shipped(self) -> None:
        print(f"  [hook] Sending shipping notification for {self.order_id}")

    def on_exit_pending(self) -> None:
        print(f"  [hook] Order {self.order_id} is no longer pending")

Happy Path

>>> order = OrderProcessor("ORD-001")
>>> order.state
'pending'

>>> order.pay(99.99)
  [hook] Order ORD-001 is no longer pending
  [hook] Sending payment confirmation for ORD-001
'Order ORD-001: payment of $99.99 received'

>>> order.ship("TRACK-12345")
  [hook] Sending shipping notification for ORD-001
'Order ORD-001: shipped (tracking: TRACK-12345)'

>>> order.deliver()
'Order ORD-001: delivered'

>>> order.state_history
['pending', 'paid', 'shipped', 'delivered']

Cancellation Path

>>> order2 = OrderProcessor("ORD-002")
>>> order2.cancel("customer changed mind")
  [hook] Order ORD-002 is no longer pending
'Order ORD-002: cancelled (customer changed mind)'

>>> order2.state
'cancelled'
>>> order2.state_history
['pending', 'cancelled']

Invalid Transition

>>> order3 = OrderProcessor("ORD-003")
>>> order3.pay(50.00)
>>> order3.ship("TRACK-99999")
>>> order3.cancel("too late")
# InvalidTransitionError: Cannot transition from 'shipped' to 'cancelled'
# via 'cancel'. Valid source state(s): paid, pending

The framework prevents cancelling an order that has already shipped — you don't need to write that check yourself.

Type Safety via @spec

Since the transition methods are also @spec-decorated, types are validated:

>>> order = OrderProcessor("ORD-004")
>>> order.pay("not a number")
# InputValidationError: Input validation failed for 'pay':
#   - Parameter 'amount': expected <class 'float'>, got str ('not a number')

What Each Layer Does

Layer Validates
@transition Current state allows this operation
@spec Arguments and return values match type hints
Hooks Side effects (notifications, logging) happen automatically
History Full audit trail of state changes

Complete Source

The full example is at examples/order_processing.py.

Why this matters for LLM-assisted development

The state machine definition is a complete specification of the workflow. An LLM can read the class and know exactly: what states exist, which transitions are valid, what arguments each transition takes, and what type it returns. There's no ambiguity about whether you can cancel a shipped order — the state machine makes it structurally impossible.