How to build an Action
The core boilerplate is pretty minimal:
class Foo
include Action
def call
# ... do some stuff here?
end
end
Declare the interface
The first step is to determine what arguments you expect to be passed into call
. These are declared via the expects
keyword.
If you want to expose any results to the caller, declare that via the exposes
keyword.
Both of these optionally accept type:
, allow_nil:
, allow_blank:
, and any other ActiveModel validation (see: reference).
class Foo
include Action
expects :name, type: String
exposes :meaning_of_life
def call
# ... do some stuff here?
end
end
Implement the action
Once the interface is defined, you're primarily focused on defining the call
method.
To abort execution with a specific error message, call fail!
.
If you declare that your action exposes
anything, you need to actually expose
it.
class Foo
include Action
expects :name, type: String
exposes :meaning_of_life
def call
fail! "Douglas already knows the meaning" if name == "Doug"
msg = "Hello #{name}, the meaning of life is 42"
expose meaning_of_life: msg
end
end
See the reference doc for a few more handy helper methods (e.g. #log
).
Customizing messages
The default error
and success
message strings ("Something went wrong" / "Action completed successfully", respectively) are technically safe to show users, but you'll often want to set them to something more useful.
There are success
and error
declarations for that -- you can set strings (most common) or a callable (note for the error case, if you give it a callable that expects a single argument, the exception that was raised will be passed in).
For instance, configuring the action like this:
class Foo
include Action
expects :name, type: String
exposes :meaning_of_life
success { "Revealed to #{name}: #{result.meaning_of_life}" }
error { |e| "No secret of life for you: #{e.message}" }
def call
fail! "Douglas already knows the meaning" if name == "Doug"
msg = "Hello #{name}, the meaning of life is 42"
expose meaning_of_life: msg
end
end
Would give us these outputs:
Foo.call.error # => "No secret of life for you: Name can't be blank"
Foo.call(name: "Doug").error # => "Douglas already knows the meaning"
Foo.call(name: "Adams").success # => "Revealed the secret of life to Adams"
Foo.call(name: "Adams").meaning_of_life # => "Hello Adams, the meaning of life is 42"
Advanced Error Message Configuration
You can also use conditional error messages with the prefix:
keyword and combine them with the from:
parameter for nested actions:
class ValidationAction
include Action
expects :input
error if: ArgumentError, prefix: "Validation Error: " do |e|
"Invalid input: #{e.message}"
end
error if: StandardError, prefix: "System Error: "
def call
raise ArgumentError, "input too short" if input.length < 3
raise StandardError, "unexpected error" if input == "error"
end
end
class ApiAction
include Action
expects :data
# Combine prefix with from for consistent error formatting
error from: ValidationAction, prefix: "API Error: " do |e|
"Request validation failed: #{e.message}"
end
# Or use prefix only (falls back to exception message)
error from: ValidationAction, prefix: "API Error: "
def call
ValidationAction.call!(input: data)
end
end
This configuration provides:
- Consistent error message formatting with prefixes
- Automatic fallback to exception messages when no custom message is provided
- Proper error message inheritance from nested actions
Message Ordering
Important: When using conditional messages, always define your static fallback messages first in your class, before any conditional messages. This ensures proper fallback behavior.
Correct order:
class Foo
include Action
# Static fallback messages first
success "Default success message"
error "Default error message"
# Then conditional messages
success "Special success", if: :special_condition?
error "Special error", if: ArgumentError
end
Lifecycle methods
In addition to #call
, there are a few additional pieces to be aware of:
Hooks
before
, after
, and around
hooks are supported. They can receive a block directly, or the symbol name of a local method.
Note execution is halted whenever fail!
is called or an exception is raised (so a before
block failure won't execute call
or after
, while an after
block failure will make result.ok?
be false even though call
completed successfully).
For instance, given this configuration:
class Foo
include Action
before { log("before hook") }
after :log_after
def call
log("in call")
end
private
def log_after
log("after hook")
raise "oh no something borked"
log("after after hook raised")
end
end
Foo.call
would fail (because of the raise), but along the way would end up logging:
before hook
in call
after hook
Hook Ordering with Inheritance:
- Around hooks: Parent wraps child (parent outside, child inside)
- Before hooks: Parent → Child (general setup first, then specific)
- After hooks: Child → Parent (specific cleanup first, then general)
This follows the natural pattern of setup (general → specific) and teardown (specific → general).
Callbacks
A number of custom callback are available for you as well, if you want to take specific actions when a given Axn succeeds or fails. See the Class Interface docs for details.
Strategies
A number of Strategies, which are DRYed bits of commonly-used configuration, are available for your use as well.