Open-source project
state-machines/state_machines avatar
state-machines/state_machines

state_machines attaches a state machine to a plain Ruby attribute

Adds support for creating state machines for attributes on any Ruby class

872 stars100 forksRubyMIT

At a glance

What is it?
The state_machines gem adds a state machine to any Ruby class by declaring it in the class body, with events, guards, callbacks, namespaced machines and state driven behaviour, and it deliberately stops short of persistence. The README is explicit that saving state to a database requires a separate integration gem such as state_machines-activerecord or state_machines-mongoid.
Who is it for?
state_machines fits a Ruby codebase that wants explicit lifecycle rules with guards and callbacks, especially one that already uses Active Record or Mongoid and will add the matching integration gem. Skip it if you need state persisted without adding a second dependency, or if you want a graph of states that is drawn and verified for you rather than declared in code.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A state machine declared in the class body, not stored for you

The gem's one line description is that it adds support for creating state machines for attributes on any Ruby class. Any Ruby class is the important phrase. There is no base class to inherit from and no model to include, so a plain Ruby object, a Rails model or a Mongoid document can all carry a state machine.

The installation is a Gemfile line and a bundle.

ruby
gem 'state_machines'
sh
bundle

Or you install it yourself.

sh
gem install state_machines

The boundary of the gem is stated in the same paragraph as the feature list, and it is the first thing to understand before you build on it. The README notes that multiple integrations are available for Active Model, Active Record, Mongoid and more in the State Machines organisation, and says plainly that if you want to save state in your database you need one of these additional integrations.

So the core gem is about behaviour and transitions, not about storage. The attribute holds a value in memory, the machine decides which values are legal and what happens on a change, and persisting that value is the job of an integration gem that knows your ORM. That split is what makes the library usable outside Rails, and it is also why a project that assumes the gem will save its state will be disappointed.

The licence is MIT, and the repository carries a `state_machines.gemspec`, a `Rakefile`, a `CHANGELOG.md` and a `lib/` directory, with `bin/` and `scripts/` directories for the tooling around the library.

Events, transitions and the guards that block them

A machine is declared with a `state_machine` call that names the attribute and the initial state.

ruby
  state_machine :state, initial: :parked do
    before_transition parked: any - :parked, do: :put_on_seatbelt

Inside the block you declare events, and each event lists the transitions it permits.

ruby
    event :repair do
      # The first transition that matches the state and passes its conditions
      # will be used
      transition stalled: :parked, unless: :auto_shop_busy
      transition stalled: same
    end

That example shows two things at once. Transitions can carry conditions, here `unless: :auto_shop_busy`, and when several transitions could apply the first one that matches the current state and passes its conditions is the one used, with the second transition acting as the fallback that always succeeds.

Conditions can also be written inline as a lambda, as in the crash event in the README, which transitions from any state except parked and stalled to stalled only `if` the vehicle has not passed inspection. The symbol form and the lambda form both exist because one is convenient for calling a method and the other is convenient for an inline expression.

The state expressions are the readable part. `all - [:parked, :stalled]` means every state except two, `stalled: same` means stay where you are, and `[:idling, :first_gear] => :parked` maps a set of source states to one target. A machine with many states therefore reads as a rule set rather than as a case statement, and the generated methods follow from the same vocabulary.

Callbacks run before, after, around and on failure

The event is the trigger, and the callbacks are where side effects live. The README's example class uses all four kinds.

ruby
    after_transition on: :crash, do: :tow
    after_transition on: :repair, do: :fix
    after_transition any => :parked do |vehicle, transition|
      vehicle.seatbelt_on = false
    end

    after_failure on: :ignite, do: :log_start_failure

    around_transition do |vehicle, transition, block|
      start = Time.now
      block.call
      vehicle.time_used += Time.now - start
    end

The `before_transition` form is the one used to attach a method to a transition without a block, and the `after_transition` block form receives the object and the transition, so the callback can inspect what happened rather than just reacting to a name.

`around_transition` is the interesting one. It receives a block, calls it, and does its own work around it, which is how the example times how long the transition took and accumulates that into `time_used`. This is the mechanism for wrapping a transition in a transaction, a lock or a timer without reimplementing the transition.

`after_failure` is the one that closes a gap people forget. A transition can fail, for instance because a guard rejected it, and the failure path is where a failed attempt gets recorded, here through `log_start_failure`.

Callbacks can also receive arguments. The README's park event reads options out of `transition.args`, popping a trailing hash, and the interaction example then calls `vehicle.park(meter_number: '12345')` and shows the attribute updated. The `transition.args` handling is a detail worth reading before you pass anything but a hash, because the example itself notes that a Rails application would normally extract the options with `extract_options!` instead.

States that carry methods and custom values

A state in this library is not just a label. The example defines behaviour inside the state block, which means the method only exists while the object is in that state.

ruby
    state :parked do
      def speed
        0
      end
    end
ruby
    state all - [:parked, :stalled, :idling] do
      def moving?
        true
      end
    end

The second block shows the state expression syntax reused for behaviour, defining `moving?` as true for every moving state, and a later block defines the same method as false for the stationary states. That is state driven instance behaviour, and it is the part of the library that changes how you organise a class, because a method that only makes sense in one state lives with that state.

The consequence is visible in the interaction example. Calling `speed` on a vehicle in a state where `speed` was never defined raises `NoMethodError: super: no superclass method 'speed'`. The error mentions `super`, which is the mechanism behind it: the generated method dispatches to the state block, and when no block defines it the lookup continues up the ancestor chain and fails. That is a deliberate design choice, and it is also a debugging cost, since the failure surfaces as a missing method rather than as an invalid state.

The second machine in the example shows namespaces and custom values.

ruby
  state_machine :alarm_state, initial: :active, namespace: :'alarm' do
ruby
    state :active, :value => 1
    state :off, :value => 0

A namespace keeps generated method names unique, so a second machine on the same class does not collide with the first, and the `value` option stores something other than the state name, here the integers 1 and 0, while the state name stays available through `alarm_state_name`.

Bang methods, fire helpers and how failures are reported

The library gives you several ways to fire an event, and they differ in what they do when a transition is not allowed.

The plain event method returns a boolean. The interaction example shows `vehicle.ignite` returning true, and a generic helper exists for firing an event without going through the instance method, `vehicle.fire_state_event(:shift_up)`, which is useful when the event name is dynamic.

The bang variant raises instead of returning false.

ruby
vehicle.park!                   # => StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear

That exception carries the event and the source state in its message, which makes it a reasonable thing to rescue in a service layer. The plain method is the right choice when a refused transition is an expected outcome, and the bang method is the right choice when it is a bug.

The predicates behave in a similar paired way. `vehicle.parked?` asks about a specific state, and there is a generic form `vehicle.state?(:parked)` that takes the state as an argument, but it is not a safe way to probe for arbitrary values.

ruby
vehicle.state?(:invalid)        # => IndexError: :invalid is an invalid name

An unknown state name raises an IndexError rather than returning false, so the generic predicate is a lookup against the machine's state list and it treats an unknown name as a programming error. The example also notes that generic state predicates can raise exceptions if the value does not exist, which is the same rule seen from the other side.

The generated query methods are part of the same surface. `can_ignite?` asks whether a transition would be permitted right now, `ignite_transition` returns the transition object that would run, and `state_events` and `state_transitions` enumerate what is currently available. The `can_` methods are what you use to grey out a button in a UI without attempting the transition.

The super() requirement and what it costs you

There is one sharp edge in the README, and it is called out twice.

ruby
    super() # NOTE: This *must* be called, otherwise states won't get initialized

The README explains the consequence in the text following the example, stating that in order for state machine attributes to be properly initialized, `super()` must be called, and pointing at `StateMachines:MacroMethods` for more information. The example's `initialize` sets a few attributes and then calls `super()` with that comment attached.

The reason is that the gem installs its own initialization logic into the class through a macro, and that logic only runs if the ancestor chain is walked. A Ruby class that defines `initialize` without calling `super` short-circuits that chain, and the state attributes come out uninitialized, with the machine left in an undefined state rather than the initial state you declared.

This is the kind of constraint that a linter or a review checklist should catch, and it is a fair argument for reading the integration gem's source before you put state machines on classes with custom constructors. A class with an existing `initialize` chain, or one that inherits from a base class with its own `initialize`, needs the call threaded through at every level that overrides it.

Everything else in the library is opt-in and explicit. The state machine is a class-level declaration, the events are methods, the callbacks are declared in the same block, and there is no global registry, no metaprogramming hook outside the class, and no configuration file. That is a good fit for codebases that prefer their behaviour to be visible in one place.

Version numbers that say the library is still moving

The release history is the last thing to read before you depend on this.

The tags are unusual, and deliberately so. The three most recent releases are state_machines/v0.202.0 on 2026-07-18, state_machines/v0.201.0 on 2026-06-12 and state_machines/v0.200.0 on 2026-06-11. A version above 0.199 in the 0.x series is a way of saying the project bumps its patch component aggressively, so a lockfile will move often without any of those moves being a breaking change.

The release automation is visible in the tree. There is a `.release-please-manifest.json`, a `release-please-config.json` and a `CHANGELOG.md`, which together describe a changelog generated from conventional commit messages, and the `.rubocop.yml` file shows the project enforces a style configuration on itself.

The repository is not archived, and the last push was on 2026-10-18, so the source is being worked on days before this evaluation. A Gemfile.lock pin is therefore the right approach, since a library that releases this often will otherwise move underneath you.

The licence is MIT, with the text in `LICENSE.txt`, so there are no obligations beyond attribution for a commercial application that uses the gem.

Editorial conclusion

state_machines fits a Ruby codebase that wants explicit lifecycle rules with guards and callbacks, especially one that already uses Active Record or Mongoid and will add the matching integration gem. Skip it if you need state persisted without adding a second dependency, or if you want a graph of states that is drawn and verified for you rather than declared in code. Verify first which integration gem matches your ORM, and confirm that your classes call super in initialize, because the README states the state attributes are not initialized otherwise.

Frequently asked questions

How do I install the state_machines gem?

Add `gem 'state_machines'` to your Gemfile and run `bundle`, or install it directly with `gem install state_machines`. The gem adds state machines to attributes on any Ruby class.

How do I persist the state with state_machines?

The core gem does not save state. The README states that if you want to save state in your database you need one of the additional integrations in the State Machines organisation, such as state_machines-activerecord, state_machines-activemodel or state_machines-mongoid.

What happens if my class does not call super in initialize?

The README notes that super() must be called, otherwise the states will not get initialized, and points at `StateMachines:MacroMethods` for details. The example class attaches that note to its own `super()` call.

How do I find out whether a transition is currently allowed?

The generated `can_` methods answer that, for example `can_ignite?` in the README example, and `ignite_transition` returns the transition object that would run. The README also lists `state_events` and `state_transitions` for enumerating what is available in the current state.

What is the difference between the plain event method and the bang version in state_machines?

The plain method returns a boolean, so `vehicle.ignite` returns true when the transition happens. The bang version raises instead, and the README example shows `vehicle.park!` raising `StateMachines:InvalidTransition: Cannot transition state via :park from :second_gear`.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. state-machines/state_machines on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/state-machines-state-machines.svg)](https://hysenlabs.com/projects/state-machines-state-machines)

Was this useful?