Aasm
AASM - State machines for Ruby classes (plain Ruby, ActiveRecord, Mongoid, NoBrainer, Dynamoid)
Install / Use
/learn @aasm/AasmREADME
AASM - Ruby state machines
Index
- Upgrade from version 3 to 4
- Usage
- Callbacks
- Guards
- Transitions
- Multiple state machines per class
- Auto-generated Status Constants
- Extending AASM
- ActiveRecord
- Bang events
- Timestamps
- ActiveRecord enums
- Sequel
- Dynamoid
- Mongoid
- Nobrainer
- Redis
- Automatic Scopes
- Transaction support
- Pessimistic Locking
- Column name & migration
- Log State Changes
- Inspection
- Warning output
- RubyMotion support
- Testing
- Installation
- Generators
- Test suite with Docker
- Latest changes
- Questions?
- Maintainers
- Contributing
- Warranty
- License
This package contains AASM, a library for adding finite state machines to Ruby classes.
AASM started as the acts_as_state_machine plugin but has evolved into a more generic library that no longer targets only ActiveRecord models. It currently provides adapters for many ORMs but it can be used for any Ruby class, no matter what parent class it has (if any).
Upgrade from version 3 to 4
Take a look at the README_FROM_VERSION_3_TO_4 for details how to switch from version 3.x to 4.0 of AASM.
Usage
Adding a state machine is as simple as including the AASM module and start defining states and events together with their transitions:
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
This provides you with a couple of public methods for instances of the class Job:
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
If you don't like exceptions and prefer a simple true or false as response, tell
AASM not to be whiny:
class Job
...
aasm whiny_transitions: false do
...
end
end
job.running? # => true
job.may_run? # => false
job.run # => false
When firing an event, you can pass a block to the method, it will be called only if the transition succeeds :
job.run do
job.user.notify_job_ran # Will be called if job.may_run? is true
end
Callbacks
You can define a number of callbacks for your events, transitions and states. These methods, Procs or classes will be called when certain criteria are met, like entering a particular state:
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
class LogRunTime
def call
log "Job was running for X seconds"
end
end
In this case do_something is called before actually entering the state sleeping,
while notify_somebody is called after the transition run (from sleeping to running)
is finished.
AASM will also initialize LogRunTime and run the call method for you after the transition from running to finished in the example above. You can pass arguments to the class by defining an initialize method on it, like this:
Note that Procs are executed in the context of a record, it means that you don't need to expect the record as an argument, just call the methods you need.
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
Parameters
You can pass parameters to events:
job = Job.new
job.run(:defragmentation)
All guards and after callbacks will receive these parameters. In this case set_process would be called with
:defragmentation argument.
If the first argument to the event is a state (e.g. :running or :finished), the first argument is consumed and
the state machine will attempt to transition to that state. Add comma separated parameter for guards and callbacks
job = Job.new
job.run(:running, :defragmentation)
In this case set_process won't be called, job will transition to running state and callback will receive
:defragmentation as parameter
Error Handling
In case of an error during the event processing the error is rescued and passed to :error
callback, which can handle it or re-raise it for further propagation.
Also, you can define a method that will be called if any event fails:
def aasm_event_failed(event_name, old_state_name)
# use custom exception/messages, report metrics, etc
end
During the transition's :after callback (and reliably only then, or in the global
after_all_transitions callback) you can access the originating state (the from-state)
and the target state (the to state), like this:
def set_process(name)
logger.info "from #{aasm.from_state} to #{aasm.to_state}"
end
Lifecycle
Here you can see a list of all possible callbacks, together with their order of calling:
begin
event before_all_events
event before
event guards
transition guards
old_state before_exit
old_state exit
after_all_transitions
transition after
new_state before_enter
new_state enter
...update state...
event before_success # if persist successful
transition success # if persist successful, database update not guaranteed
event success # if persist successful, database update not guaranteed
old_state after_exit
new_state after_enter
event after
event after_all_events
rescue
event error
event error_on_all_events
ensure
event ensure
event ensure_on_all_events
end
Use event's after_commit callback if it should be fired after database update.
The current event triggered
While running the callbacks you can easily retrieve the name of the event triggered
by using aasm.current_event:
# taken the example callback from above
def do_something
puts "triggered #{aasm.current_event}"
end
and then
job = Job.new
# without bang
job.sleep # => triggered :sleep
# with bang
job.sleep! # => triggered :sleep!
Guards
Let's assume you want to allow particular transitions only if a defined condition is
given. For this you can set up a guard per transition, which will run before actually
running the transition. If the guard returns false the transition will be
denied (raising AASM::InvalidTransition):
class Cleaner
include AASM
aasm do
state :idle, initial: true
state :cleaning
event :clean do
transitions from: :idle, to: :cleaning, guard: :cleaning_needed?
end
event :clean_if_needed do
transitions from: :idle, to: :cleaning do
guard do
cleaning_needed?
end
end
transitions from: :idle, to: :idle
end
event :clean_if_dirty do
transitions from: :idle, to: :cleaning, guard: :if_dirty?
end
end
def cleaning_needed?
false
end
def if_dirty?(status)
status == :dirty
end
Related Skills
node-connect
345.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
104.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
