# AASM: finite state machines for plain Ruby objects, not just ActiveRecord

> The Ruby gem that started as acts_as_state_machine and grew into a persistence-agnostic state machine library, with adapters for ActiveRecord, Sequel, Mongoid, Redis and arbitrary classes.

**aasm/aasm** — AASM - State machines for Ruby classes (plain Ruby, ActiveRecord, Mongoid, NoBrainer, Dynamoid)

- Repository: https://github.com/aasm/aasm
- Stars: 5,231 · Forks: 647
- Language: Ruby
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/aasm-aasm

## From acts_as_state_machine to a library that does not need ActiveRecord

The README opens with the lineage, and it explains most of the design. AASM began as the acts_as_state_machine plugin and evolved into a more generic library that no longer targets only ActiveRecord models. It currently provides adapters for many ORMs, and it can be used for any Ruby class regardless of parent class, including none.

That sentence is the one to sit with if you have seen older versions of this gem. The ActiveRecord integration is not the architecture with a pluggable backend; the state machine is the architecture, and persistence is an optional layer. The README's index makes the same point structurally, listing the plain Ruby features first, among them callbacks, guards, transitions, multiple state machines per class, auto-generated status constants and extending AASM, and only then the adapter sections for ActiveRecord, Sequel, Dynamoid, Mongoid, Nobrainer and Redis.

The repository layout matches that order. Alongside lib/, spec/ and test/ there is API, HOWTO, TESTING.md, CHANGELOG.md, PLANNED_CHANGES.md, and two upgrade guides, README_FROM_VERSION_3_TO_4.md and README_FROM_VERSION_5_TO_6.md. The presence of a planned changes file alongside two major-version upgrade notes says something about where the project is: it is past rapid API churn and into the phase where changes are announced before they land. Topics on the repository include activerecord, mongoid, rails, ruby, state-machine, transition and hacktoberfest, and the default branch is master. Licence is MIT, with 5,233 stars, 645 forks and 169 open issues, last pushed 2026-07-05.

## States, events and transitions in one block

Defining a machine is a single block on a class. States are declared, one is marked initial, and events carry the transitions between them:

```ruby
class Job
  include AASM

  aasm do
    state :sleeping, initial: true
    state :running, :cleaning

    event :run do
      transitions from: :sleeping, to: :running
    end

    event :clean do
      transitions from: :running, to: :cleaning
    end

    event :sleep do
      transitions from: [:running, :cleaning], to: :sleeping
    end
  end

end
```

The instance methods that fall out of that are the part of the API you touch most:

```ruby
job = Job.new
job.sleeping? # => true
job.may_run?  # => true
job.run
job.running?  # => true
job.sleeping? # => false
job.may_run?  # => false
job.run       # raises AASM::InvalidTransition
```

Two conventions matter here. Predicate methods are generated per state, so sleeping? and running? are not something you write. And may_ prefixed predicates answer whether an event is currently possible, which is what you want for driving interface state: a button that is disabled when may_run? is false rather than a rescue block around the click.

Events can take arguments, and those arguments reach every guard and after callback:

```ruby
  job = Job.new
  job.run(:defragmentation)
```

There is also a convention about the first argument. If it is a state symbol such as :running or :finished, the machine consumes it and attempts to transition to that state directly, then treats the remaining comma separated arguments as the event parameters. It is a small detail, and it is the kind of detail worth knowing before you write an event that takes a symbol.

And when firing an event you can pass a block that runs only if the transition succeeds, which keeps the side effect tied to the transition rather than adjacent to it:

```ruby
  job.run do
    job.user.notify_job_ran # Will be called if job.may_run? is true
  end
```

## Callbacks come in five shapes, including classes with initialize

Callbacks can attach to events, transitions or states, and the README demonstrates every accepted shape in one Job class: a symbol, a Proc, an after_all_transitions hook, an inline before block, an after transition Proc with splat args, a class, an inline after block, and an error handler.

```ruby
class Job
  include AASM

  aasm do
    state :sleeping, initial: true, before_enter: :do_something
    state :running, before_enter: Proc.new { do_something && notify_somebody }
    state :finished

    after_all_transitions :log_status_change

    event :run, after: :notify_somebody do
      before do
        log('Preparing to run')
      end

      transitions from: :sleeping, to: :running, after: Proc.new {|*args| set_process(*args) }
      transitions from: :running, to: :finished, after: LogRunTime
    end

    event :sleep do
      after do
        ...
      end
      error do |e|
        ...
      end
      transitions from: :running, to: :sleeping
    end
  end

  def log_status_change
    puts "changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})"
  end

  def set_process(name)
    ...
  end

  def do_something
    ...
  end

  def notify_somebody
    ...
  end

end
```

The class form is the part with a real gotcha. AASM will instantiate a class you pass as a callback and call its call method, so the object needs an initialize that accepts the record instance as its first parameter. The README states that the args parameter is optional, but if you define initialize you must accept the model instance first:

```ruby
class LogRunTime
  # optional args parameter can be omitted, but if you define initialize
  # you must accept the model instance as the first parameter to it.
  def initialize(job, args = {})
    @job = job
  end

  def call
    log "Job was running for #{@job.run_time} seconds"
  end
end
```

That LogRunTime object is registered as the after callback on the running to finished transition, and its call method logs how long the job ran. The other convention is about Procs: they execute in the context of the record, so they do not take the record as an argument and can call instance methods directly. The aasm accessor is also part of the surface, since log_status_change reads aasm.from_state, aasm.to_state and aasm.current_event rather than instance state.

The whiny toggle is the other behaviour switch. If you would rather have a boolean than an exception, set whiny_transitions: false on the machine, after which a refused event returns false and may_run? tells you why the call would not have worked.

## ActiveRecord and the other persistence layers

The adapter sections are where AASM meets the database, and the index lists enough of them to show the shape: ActiveRecord, ActiveRecord enums, Sequel, Dynamoid, Mongoid, Nobrainer, Redis, automatic scopes, transaction support, pessimistic locking, column name and migration, and log state changes.

Automatic scopes is the feature that most changes day-to-day model code. Declare scopes and the library creates reader methods per scope, so a model with cleaned and uncleaned scopes gets cleaned? and uncleaned? without you writing the query twice.

Transaction support and pessimistic locking are the two that decide whether a state change is safe under concurrency. A state machine that raises on an illegal transition is only useful if two concurrent requests cannot both read running and both proceed, and those sections are where that guarantee lives.

Bang events sit in the index too. The ordinary event raises AASM::InvalidTransition when it is not permitted, and the bang variant is the non-raising form, which is convenient for fire-and-forget callers such as a controller responding to a stale page rather than a service object that wants to know.

Sequel, Dynamoid, Mongoid, Nobrainer and Redis are documented with the same weight as ActiveRecord, which follows from the architecture: the machine is not stored in a column by definition, it is just integer or symbol state, and the adapter decides how that survives a round trip. The RubyMotion section shows the library being used outside MRI entirely.

For testing there is a dedicated README section with RSpec and Minitest paths, and Minitest gets its own assertions and expectations subsections. ActiveRecord enums get their own section as well, which matters for anyone whose model already uses enum for the state column and does not want a second source of truth.

## Inspection, warning output, and a test suite that boots four datastores

Two smaller features are easy to overlook and quietly useful. Inspection gives you the machine's current state from outside the model, which is what you want in a debugger or an admin screen. Warning output covers the case where a transition is attempted that the developer probably did not intend, so a development environment can complain instead of silently returning false.

Multiple state machines per class is the other structural feature worth planning around, and the index carries two subsections on it: handling naming conflicts between multiple state machines, and binding an event. If your model has two independent lifecycles, for example a publication state and a moderation state, AASM can hold both, but the naming has to be disambiguated and events have to be bound to the right machine. Getting this wrong is the usual source of surprise, so it is worth reading that subsection before defining a second machine rather than after.

The test infrastructure explains the breadth of the adapter support better than any claim could. The repository ships a Dockerfile based on ruby:2.3.4-slim, a docker-compose.yml, an Appraisals file and a gemfiles/ directory, and the compose service runs the full matrix:

```
bundle exec appraisal install && bundle exec appraisal rspec
```

That command runs against DynamoDB local, MongoDB 3.6.1, Redis 4.0.6 alpine and RethinkDB 2.3.6, wired through environment variables in the compose file. Running four datastores in CI is expensive, and it is the price of the promise that the library is persistence agnostic rather than ActiveRecord shaped. It is also, quietly, a statement about the project's priorities: the adapters are not decorative.

Installation is documented three ways, manually from RubyGems.org, through Bundler, and building your own gems from the source, with generators listed separately for scaffolding. The repository publishes no GitHub releases, so the gem version and the changelog are the fastest way to see where a checkout stands, and README_FROM_VERSION_5_TO_6.md plus README_FROM_VERSION_3_TO_4.md are the two documents to read before upgrading an older codebase.

## Conclusion

AASM is a mature answer to a question that keeps recurring in Rails codebases, and its maturity shows up in the unglamorous places: a document dedicated to the 3 to 4 upgrade, a second one for 5 to 6, a planned changes file, and a test suite that boots four different datastores at once because four different adapters must keep working. Two design choices are worth deciding on before you adopt it. First, it targets any Ruby object, not just models, so a plain class with no parent can still hold a state machine, and the persistence adapters are optional rather than the point. Second, transitions raise AASM::InvalidTransition by default, and turning that off with whiny_transitions: false changes your control flow from exceptions to booleans, which is a decision to make once per class rather than per call site. If you are evaluating it, define one real lifecycle in a single aasm block with one event per user-visible action, use may_ predicates to drive interface state, and keep the ActiveRecord-specific parts (timestamps, scopes, enums) out of the block until the plain Ruby version works.

## FAQ

### What does AASM stand for in the Ruby gem?

It began as the acts_as_state_machine plugin and is now simply AASM, a library for adding finite state machines to Ruby classes. The README notes it evolved into a more generic library that no longer targets only ActiveRecord models.

### Can AASM be used without ActiveRecord?

Yes. AASM can be used for any Ruby class regardless of its parent class, including none, and adapters for ActiveRecord, Sequel, Mongoid, Dynamoid, Nobrainer and Redis are optional rather than required.

### How do I stop AASM events from raising exceptions?

Declare the machine with whiny_transitions: false. A refused event then returns false instead of raising AASM::InvalidTransition, and you can ask first with the generated may_ prefixed predicates such as may_run?.

### How do I pass arguments to an AASM event?

Pass them to the event method, for example job.run(:defragmentation), and every guard and after callback receives them. If the first argument is a state such as :running or :finished, it is consumed and the machine attempts to transition to that state directly.

### Does AASM have an official way to run its whole test suite?

Yes, through the bundled Docker setup. The compose service runs bundle exec appraisal install && bundle exec appraisal rspec against DynamoDB local, MongoDB, Redis and RethinkDB so every persistence adapter is exercised.

## Sources

- [aasm/aasm on GitHub](https://github.com/aasm/aasm)
- [Issues](https://github.com/aasm/aasm/issues)
- [License: MIT](https://github.com/aasm/aasm/blob/master/LICENSE)
- [README](https://github.com/aasm/aasm/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/aasm-aasm
