The project is in a healthy, maintained state
A minimal state machine for Ruby objects. Define states, events, transitions, guard conditions, and callbacks with a clean DSL. Works with any Ruby class — no framework dependency required.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

philiprehberger-state_machine

Tests Gem Version Last updated

Lightweight state machine DSL with transitions, guards, callbacks, history tracking, auto-transitions, parallel states, statistics, and graph export

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-state_machine"

Or install directly:

gem install philiprehberger-state_machine

Usage

Basic State Machine

require "philiprehberger/state_machine"

class Order
  include Philiprehberger::StateMachine

  state_machine initial: :pending do
    event :pay do
      transition from: :pending, to: :paid
    end

    event :ship do
      transition from: :paid, to: :shipped
    end

    event :deliver do
      transition from: :shipped, to: :delivered
    end
  end
end

order = Order.new
order.current_state  # => :pending
order.pay!
order.current_state  # => :paid

Guards

Attach a guard lambda to conditionally block transitions:

class Order
  include Philiprehberger::StateMachine

  attr_accessor :tracking_number

  state_machine initial: :pending do
    event :pay do
      transition from: :pending, to: :paid
    end

    event :ship do
      transition from: :paid, to: :shipped, guard: -> { !tracking_number.nil? }
    end
  end
end

order = Order.new
order.pay!
order.ship              # => false (no tracking number)
order.tracking_number = "TRACK123"
order.ship!             # => transitions to :shipped

Callbacks

Register before and after callbacks with optional state filters:

class Order
  include Philiprehberger::StateMachine

  state_machine initial: :pending do
    event :pay do
      transition from: :pending, to: :paid
    end

    before_transition to: :paid do |order|
      puts "About to mark order as paid"
    end

    after_transition to: :paid do |order|
      puts "Order is now paid"
    end
  end
end

Introspection

Query the state machine at runtime:

order = Order.new

order.pending?            # => true
order.paid?               # => false
order.can_pay?            # => true
order.can_ship?           # => false
order.allowed_transitions # => [:pay]

State History

Track previous states with timestamps:

order = Order.new
order.pay!
order.state_history
# => [{state: :pending, entered_at: <Time>}, {state: :paid, entered_at: <Time>}]
order.previous_state  # => :pending

Timed/Automatic Transitions

Define transitions that trigger automatically after a time period:

class Session
  include Philiprehberger::StateMachine

  state_machine initial: :active do
    event :refresh do
      transition from: :active, to: :active
    end

    auto_transition from: :active, to: :expired, after: 1800
  end
end

session = Session.new
# Later, check if any auto-transitions should fire:
session.check_auto_transitions!  # => true if expired, false otherwise

Parallel States

Activate concurrent substates during a transition:

class Upload
  include Philiprehberger::StateMachine

  state_machine initial: :idle do
    event :start do
      transition from: :idle, to: :processing
      parallel_states :uploading, :validating
    end

    event :finish do
      transition from: :processing, to: :done
    end
  end
end

upload = Upload.new
upload.start!
upload.parallel_states              # => [:uploading, :validating]
upload.parallel_state_active?(:uploading)  # => true
upload.finish!
upload.parallel_states              # => []

Transition Statistics

Track transition counts and time spent in each state:

order = Order.new
order.pay!
order.transition_count          # => 1
order.time_in_state(:pending)   # => 0.003 (seconds)
order.transition_stats
# => {total_transitions: 1, transition_counts: {pending_to_paid: 1}, time_in_states: {...}}

State Lifecycle Hooks

Fire callbacks when entering or leaving a specific state, regardless of which event triggered the transition:

state_machine initial: :idle do
  on_enter(:active) { |obj| obj.log("Entered active") }
  on_exit(:idle) { |obj| obj.log("Left idle") }

  event :start do
    transition from: :idle, to: :active
  end
end

Time in Current State

obj.time_in_current_state  # => 12.5 (seconds)

DOT/GraphViz Export

Generate a visual state diagram in DOT format:

puts Order.to_dot
# digraph Order {
#   rankdir=LR;
#   __start__ [shape=point, width=0.2];
#   __start__ -> pending;
#   pending [shape=ellipse];
#   ...
# }

Render with GraphViz: dot -Tpng -o states.png

Event Payload

Pass keyword arguments when firing events. Payloads are forwarded to guards, callbacks, and hooks:

class Order
  include Philiprehberger::StateMachine

  state_machine initial: :pending do
    event :pay do
      transition from: :pending, to: :paid, guard: ->(amount:, **) { amount > 0 }
    end

    after_transition to: :paid do |obj, payload|
      puts "Paid #{payload[:amount]}"
    end
  end
end

order = Order.new
order.pay!(amount: 100)      # guard receives amount:, callback receives {amount: 100}
order.can_pay?(amount: 50)   # => true
order.can_pay?(amount: 0)    # => false

Existing guards and callbacks with no payload parameter continue to work unchanged.

Unreachable State Detection

Validate that all states can be reached from the initial state:

Order.unreachable_states  # => [] (all reachable)

API

Method Description
state_machine(initial:, &block) Define a state machine on the class with an initial state
event(name, &block) Define an event inside the state machine block
transition(from:, to:, guard: nil) Define a transition inside an event block
parallel_states(*states) Activate concurrent substates during a transition
auto_transition(from:, to:, after:, guard: nil) Define a timed automatic transition
before_transition(to: nil, from: nil, &block) Register a callback that fires before a transition
after_transition(to: nil, from: nil, &block) Register a callback that fires after a transition
#current_state Returns the current state as a symbol
#can_X?(**payload) Returns true if event X can fire from the current state (including guards)
#allowed_transitions(**payload) Returns an array of event names that can fire from the current state
#X!(**payload) Fire event X with optional payload, or raise InvalidTransition
#X(**payload) Fire event X with optional payload, returns true on success, false on failure
#X? Returns true if current state is X
#state_history Returns array of {state:, entered_at:} hashes
#previous_state Returns the state before the current one, or nil
#transition_count Returns total number of transitions performed
#time_in_state(state) Returns seconds spent in the given state
#transition_stats Returns hash with counts and timing for all transitions
#parallel_states Returns array of currently active parallel substates
#parallel_state_active?(state) Returns true if the given substate is active
on_enter(state, &block) Callback fired when entering a state
on_exit(state, &block) Callback fired when leaving a state
#time_in_current_state Seconds spent in current state
#check_auto_transitions! Fires any pending auto-transitions, returns true if one fired
.to_dot(name:) Generates DOT/GraphViz string for the state machine
.unreachable_states Returns array of states unreachable from initial

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT