Docs in three places:

Concepts (dashboard index, create comment)

Operation implement functions of the app (also called commands) and it use:

  • policy - authorizations: Policy::Guard macro
  • representer - cell(widget) instance methods are used in view model (no need for view context)
  • validation - forms or contracts: type check and rules
  • model - only associations and scopes
  • callback

Operation Simple example

class Song::Create < Trailblazer::Operation
  step :assign_current_user!
  step :debug


  def assign_current_user!(ctx, model:, current_user:, **)
    model.created_by = current_user

  def debug(ctx, params:, **)

Operation always return result object. It is called using short notation, instead a you can write ruby alias for it Operation.(a)

result = Song::Create.(params.merge current_user: current_user)
result['model'] # => #<Song ...>

It is using step method which can be instance method, lambda or callable model

step :model!
step ->(ctx, **) { ctx['model'] = }
step MyStep

def model!(ctx, **)
  ctx['model'] =

# define Callable
class MyStep
  extend Uber::Callable
  def, **)
    ctx['model'] =
    # return value of step matters and is should not be nil or false

If step return value is falsey (nil or false) than operation result is marked as failed and only steps marked as fail (failure for trb 2.0) after failing step will be executed. You can pass fail_fast failure :abort, fail_fast: true if you do not want to execute other failure steps after this this. Instead of step you can use pass (2.1 success) which does not care about return value.

Step accepts first param options or ctx or skill (this should act like accumulator since we can write output to it and it is mean to be mutable). In trailblazer 2.0 first params passed to .call was inside ctx['params']. In trb 2.1 you need to explicitly call using params result = Memo::Create.(params: params, current_user: current_user). first-value and second-hash params were merged to ctx. At the end context is converted to result object on which you can access result['model'] and other result['current_user'].

Second argument can extract keys from first, for example, you can write to options in each step ctx[:new_result] = and you can extract in following steps with def step(ctx, new_result:, **) and so make it required. If you state them in method definition for very first step, than those arguments will be required keys in first hash in trailblazer 2.1 (trailblazer 2.0 it should be second hash argument). double splat ** means “I know there are more keyword arguments but I’m not intereseted right now”. You can use name: ' to override step name


Model source is defined in trailblazer-macro gem (not trailblazer-operations)

step Model(Song, :new) # this is the same as ctx[:model] =
step Model(Song, :find_by) # this is the same as ctx[:model] = Song.find_by params[:id]
step Model(Song, :find) # find params[:id]
# There will be automatic jump to error track if can not find the model

Nested can call other operations

class Update < Trailblazer::Operation
  step Nested( Edit )

  # you can decide which step to perform in runtime
  step Nested( :build! )
  def build!(ctx, current_user:. **)
    current_user.admin? ? Create::Admin ? Create::NeedsModeration

  # you can adapt parameters using input param
  step Nested( Multiplier, input: ->(options, some_data:, **) do
    { x: some_data[:pi_constant], y: 2 }

  # you can also pick specific items from nested context (not all are needed)
  step Nested( Edit, output: ->(options, mutable_data:, **) do
      '' => mutable_data['contract.default'],
      'model'       => mutable_data['model']


step Policy::Guard( :authorize! )

and you can access results in


Contract::Build is defined in trailblazer-macro-contract gem

Macro API

Macro is a capitalized name function which returns two element array: actual step, and default options

module Macro
  def self.MyPolicy(allowed_role: "admin")
    step = ->(input, options) { options["current_user"].type == allowed_role }

    [ step, name: "my_policy.#{allowed_role}" ] # :before, :replace, etc. work, too.

You can use like

class Create < Trailblazer::Operation
  step Macro::MyPolicy( allowed_role: "manager" )
  # ..

Reform Contract

Example contract form validation using Reform gem

gem 'reform' # it depends on disposable gem which provide a Twin decorator, and
             # depends on representable gem which provide representer
             # deserializer
             # validation is in dry-validation or Activemodel::Validations

gem 'dry-validation'
# for rails Activemodel::Validations
gem 'reform-rails'

Actual validation can be implemented using Reform (with ActiveModel::Validation or dry-validation) or using Dry::Schema (without Reform). It allows mapping to nested models, via composition, to hash fields. It is initialized with model for which you want to validate data against, and once read, original model’s values will never be accessed. To save to model you can run which will call sync and Main purpose of form object is to not let form builders (when rendering), validators (when validating) and writers (when acceptings params) to access the model.

form = Blog.last
form.title # this will return Blog.last.title
form.title = 'new' # this will not change blog title
form.model.title # this old value

result = form.validate title: 'new title'

Here is example of reform

# app/concepts/song/contract/create.rb
# no need to include if using rails
# require 'reform'
# require 'reform/form/dry'
# include Dry
module Song::Contract
  class Create < Reform::Form
    property :title

For hash it use this property :album, field: :hash

Properties can be virtual: true so that reform will not read, nor write to model (it is used only in operation run). writeable: false means that it will not write to model. To prevent writing only on deserializing phase from params, but allow to write in operation step and to sync with model

property :image_meta_data, deserializer: { writeable: false }

Populator is called when validate is triggered, but before deserialisation and actual validation happens and ONLY when that attribute is present in validation params. populate_if_empty is using populator. For collections it will be called for each fragment, it receive options hash, and :frament and :index can be extracted. If we have populator for single value, fragment will be string, if we populate collection or object (json column, or associated object) fragment will be hash or array. Use return skip! to stop deserializing current fragment. When populate_if_empty is called return value is imporant since it is used to initialize property (which could be nested form). Note that populator is not called (triggered) if we call .validate my_prop: nil ie validate again value nil or key does not exists, but it will be called if key exists and value is something not nil (false, string , number). Inside populator method we need to set the form manually using self and respective setter self.artist = (return value is not used automatically as in populate_if_empty).

property :artist, populator: ->(fragment:) { self.artist = Artist.find_by(id:
fragment) } To manually populate property which does not exists in params, you can use parse_pipeline, or step :setup_params in operation

property :name,
        deserializer: { parse_pipeline: ->(*) { ->(input, represented:, **) { ... }} }

For has_one and has_many associations (which use collections) prepopulators are used for rendering (for new action) and must be invoked manually @form.prepopulate! and since it is invoked manually it will blindly run the block and create nested form around whatever you instantiated in the block, It is used for presentation ie show empty fields (not for validation of the form).

property :user, prepopulator: ->(*) { self.user = },

For validation population use populate_if_empty which is run automatically before deserialisation and actual validation happens. It is called for every hash fragment that does not have a nested form yet return value of populate_if_empty will be automatically wrapped in a nested form and added to the main form. Note that params optionally have _attributes sufix with STRING keys for both top properties and nested collection properties for example both are valid params: { song: { 'users_attributes' => { '0' => { 'email' => '[email protected]'}}}}, or { song: { users: [{ 'email' => '' } ]} }. inside method you can access options[:fragment]['email'] ie hash for that part and with string as keys (not :email symbols)

collection :users,
  skip_if: :all_blank, # dynamically decide whether or not an incoming hash
                       # should be deserialized to populate the form
  populate_if_empty: ->(*) { } # this populator only runs when there
                            # is incoming hash, but no corresponding nested form
  populator: :populate_user! do

  property :email
  validates email, presence: true

def populate_user!(fragment:, **)
  self.user = (User.find_by(email: fragment["email"]) or

Note that we can use ctx[:model].build_user in custom :populate_association step in operation (static way of population) so we don’t need prepopulator nor populate_if_empty.

To validate properties, you can use groups of validation so we run them after or if some other group is finished.

Inside validation group you can use dry validation schema required(:title).filled see old or new To define custom predicates you need to use configure. If you need access to whole form you can add with: { form: true }

  validation do
    configure with: { form: true } do
      # inside this method we have access to `form`
      def unique?(value)
        Song.where(name: value).not(id:
    required(:body).maybe(min_size?: 9)

and can be used in operation with this API (initialize, validate, save, errors, sync, prepopulate) but we can use macros: Build, Validate, Persist for example in rails c you can run and see default contract

res = Venue::Operation::Create.(params: {})

Using Macros will simplify the code:

step Contract::Build( constant: Venue::Contract::Create )
# this will populate contract.default (and result.contract.default) with
# ctx[:model]] like this line
# ctx['contract.default'] =[:model])

step Contract::Validate( key: :venue )
# this will fetch contract.default and valide against key
# reform_contract = ctx["contract.default"]
# result = reform_contract.validate(ctx["params"][:venue])
# error will be available in result['result.contract.default'].errors.messages
# in reform 2.2.4 in result['contract.default']

step Contract::Persist()
# this will save contract data. Note that it will not change ctx['model']
# reform_contract = ctx["contract.default"]

Form is using model persist? method form.persisted? is actual form[:model].persisted?. Sometimes in firefox, when you navigate back, it remembers _method input with different value than patch and error Routing Error No route matches [POST] is raised. Solution is to hard refresh

Twins method form.created? will return true if decorated model was just created ie persisted status changed from false to true. To use that we feature

module Thing::Contract
  class Create < Reform::Form
    feature Disposable::Twin::Persisted

We can use added for collections to check which was added using << and not by pulling from db on initialize state. Imperative Callbacks is to implicitly write callback invocation where you need it, twin object graph collects low level events like adding/deleting/creating/updating/destroying.

Reform is creating object graph from arbitrary input (it can wrap existing or new models), validate object graph and process it and than sync to models.


incoming document is params, and output is represented object (any object that expose property setters) Example

Band =
class SongRepresenter < Representable::Decorator
  include Representable::Hash

  property :title
  property :band, class: Band do
    property :name


Song =, :band)
song = 'Duke')
# it will do: song.title = params[:title]

input = { "title" => "Let Them Eat War", "band" => { "name" => "Bad Religion" } }  #=> #<struct Band name="Bad Religion"> #=> "Bad Religion"

or usage in operation in Validate() using :representer key

  step Contract::Validate( representer: MyRepresenter )

Example for rendering (which should be outside of operation)

result = Create.(params, document: '')
if result.success?

Custom steps

# app/concepts/venue/lib/notification.rb
class Venue::Notification

Invoke with

step :notify!

def notify!
  ctx['result.notify'] = Venue::Notification.(current_user, model)


/home/orlovic/rails/temp/trb_test Gemfile

# reform is not ready for dry-validation ver 1 so fix to 0
gem 'dry-validation', '~> 0.10.6'
gem 'dry-types', '~> 0.10.2'

gem 'trailblazer', '>= 2.0.3'
gem "trailblazer-cells"
gem 'trailblazer-generator', require: false
gem 'trailblazer-rails' # this will include trailblazer-loader

gem 'cells-rails'
gem "cells-slim"
# gem 'cells-hamlit'

gem 'reform-rails'

It will load files from app/concepts (trailblazer-loader gem). It will load files for example app/concepts/product/operation/create.rb and you can use Product::Create class name (because trailblazer-loader do not use Rails autoloader and no need to use Production::Operation::Create). It provides run Blog::Index method that will invoke the operation, pass params and if you want, also dependencies as current_user using _run_options

class ApplicationController < ActionController::Base
  def _run_options(options)
    options.merge current_user: current_user

Instead of run Operation {block} you can use oposite method Operation.reject() {block} so block will be executed if operation is failed.

It sets @model (also @blog which is identical to @model) and @form if exists (if Contract:Build was called and initialize in result['contract.default']) and result object (by convention operation should populate result['model'] object or array for index operation).

If operation can go wrong (success and error track) than you can use the block syntax run Venue::Update do |result| which will be called on success and you should return redirect_to path so the code after run will not be executed (fail path).

Also it extends render to allow rendering cells

render cell(Blog::Cell::Index, @form), layout: false

All file and class names should be singular. Here is example controller

# app/controllers/venues_controller.rb
class VenuesController < ApplicationController
  def new
    run Venue::Create::Present
    render cell(Venue::Cell::New, @form), layout: false

  def create
    run Venue::Create do |result|
      return redirect_to venues_path

    render cell(Venue::Cell::New, @form), layout: false

  def show
    run Venue::Show
    render cell(Venue::Cell::Show, result["model"]), layout: false

  def index
    run Venue::Index
    render cell(Venue::Cell::Index, result["model"]), layout: false

  def edit
    run Venue::Update::Present
    render cell(Venue::Cell::Edit, @form), layout: false

  def update
    run Venue::Update do |result|
      flash[:notice] = "#{result["model"].title} has been saved"
      return redirect_to venue_path(result["model"].id)

    render cell(Venue::Cell::Edit, @form), layout: false

  def destroy
    run Venue::Delete

    flash[:alert] = "Venue deleted"
    redirect_to venues_path

To show data from operation in the view, you can use result object

# step :a?
def a?(ctx, **)
  if true
    ctx[:error_message] = 'Hi'

result = run MyOp
flash[:alert] = result[:error_message]


Use minitest-rails-capybara gem for testing rails generate minitest:feature CanAccessHome --no-spec gives

require "test_helper"

class CanAccessHomeTest < Capybara::Rails::TestCase
  def test_homepage_has_content
    visit root_path
    assert page.has_content?("Home#index")

but I do not see why we will use it when we have system test


This View-model uses helpers that are not globals, but instance methods. It is faster than ActionView because there is no code.

# in controller
render cell(Venue::Cell::New, @form), layout: false

# in console
html = Venue::Cell::New.(, current_user: User.last).()

# in view
<%= concept('venue/cell', venue) %>
# this will call:

# to render collection which will be joined into one html
concept('venue/cell', collection: venues)

Template name is generated from method name, you can call render :new to render app/concept/venue/view/new.slim. Use cells-slim

# app/concepts/venue/cell/new.rb
module Venue::Cell
  class New < Trailblazer::Cell
    # to use t('hi')
    include ActionView::Helpers::TranslationHelper
    # to use bootstrap_form_for
    include BootstrapForm::ActionViewExtensions::FormHelper

Extend cell with other methods. For different request format (html, js, json) you should have different method than show, for example concept('venue/cell', venue).(:append)

class Comment::Cell < Cell::Concept
  class Grid < Cell::Concept
    include ActionView::Helpers::JavaScriptHelper

    def show
      # concept('comment/cell', collection: comments).to_s + paginate(comments).html_safe
      render :grid

    def append
        var el = document.getElementById('next')
        el.outerHTML = "#{j show}"

Inside methods you can access model and options object (other parameters that you passed to cell and also option[:context][:controller] where concept was used ).

# app/concepts/venue/cell/edit.rb
module Venue::Cell
  class Edit < New
    def show
      render :new

    def back
      link_to "Back", venue_path(

    def delete
      link_to "Delete venue", venue_path(, method: :delete

You can delegate title to context[:model].title using property class method.

# app/concepts/venue/cell/show.rb
module Venue::Cell
  class Show < Trailblazer::Cell
    property :title
    property :body

    def current_user
      return ctx[:context][:current_user]

    def time

    def edit
      link_to "Edit", edit_venue_path(

    def delete
      link_to "Delete", venue_path(, method: :delete, data: {confirm: 'Are you sure?'}

    def back
      link_to "Back to index", venues_path
# app/concepts/venue/cell/item.rb
module Venue::Cell
  class Item < Trailblazer::Cell
    def title
      link_to model.title, model unless model == nil

    property :body

    def created_at
      model.created_at.strftime("%d %B %Y")
# app/concepts/venue/cell/index.rb
module Venue::Cell
  class Index < Trailblazer::Cell
    def total
      return "No venues" if model.size == 0

When I try to render cell in console it crashes since no context[:controller] is given and url_options are needed

html = Venue::Cell::New.(, current_user: User.last).()
=> "<div id=\"remote-form\"></div>"

# but if I have a root_path than error is raised
NoMethodError (undefined method `[]' for nil:NilClass)

# if I call with blank context than url_options error is given
html = Venue::Cell::New.(, current_user: User.last, context: {}).()
NoMethodError (undefined method `url_options' for nil:NilClass)

# so solution is to call using fake controller
html = Venue::Cell::New.(, current_user: User.last, context: {controller:{})}).()

To use in test you need to inherit Cell::TestCase and use cell

# test/cell/venue/new_test.rb
require 'test_helper'
class VenueCellNewTest < Cell::TestCase
  test 'renders' do
    venue = venues(:novi_sad)
    html = cell(Venue::Cell::New, venue).()
    assert_match /#{}/, html.to_s

but I also get error for any line like root_path

NoMethodError: undefined method `url_options' for nil:NilClass
    /home/orlovic/.rvm/rubies/ruby-2.6.3/lib/ruby/2.6.0/forwardable.rb:228:in `url_options'
    actionpack ( lib/action_dispatch/routing/route_set.rb:265:in `call'

so when I step into cell which will call cell_for args so found than it uses context of controller_class can also pass fake controller

In rails

Caching in cells

# app/concepts/thing/cell.rb
class Thing::Cell < Cell::Concept
  include Cell::Caching::Notifications

  class Grid < Cell::Concept
    cache :show


Disposable Twins API use dry-types

Twin is an intermediate object (decorator) between model and application params.

class AlbumTwin < Disposable::Twin
  property :title

Dry types is used by dry schema and dry struct. Used for value coercions, applying constrains, define complex structs or value objects. After installing with gem install 'dry-types' 'dry-struct' you can test in irb

require 'dry-types'
require 'dry-struct'

module Types
  include Dry.Types()
  # this will provide constants Decimal, Bool, String... Strict::String...
  # Coercible::String...

User = Dry.Struct(
  name: Types::String, # Types is our module
  age: Types::Integer,
# or
class User < Dry::Struct
  attribute :name, Types::Strict::String # Types is our module
end name: 'Bob', age: 35

You can use [] to pass directly without creating an object Types::Coercible::Integer['1a'] # Dry::Types::CoercionError (invalid value for Integer(): "1a")

To see all types

 => [:String, :Decimal, :Float, :Hash, :Bool, :DateTime, :Date, :Range, :Class, :Any, :Symbol, :Time, :Integer, :Nil, :True, :Array, :False]

Types.constants - Types::Nominal.constants
 => [:Optional, :Params, :Coercible, :JSON, :Nominal, :Strict]

Categories of types are

  • Nominal base type definitions with a primitive class
  • Strict raise error if passed attribute of wrong type
  • Coercible attempt to convert to the correct class using Ruby coercion ('1' will be 1 but '1a' will raise an ArgumentError: invalid value for Integer)
  • .optional attribute value can be nil (alternative to Maybe)
  • .constrained(gteq: 18) add custom contraints
  • .meta(info: 'some info') add metadata
  • .default('draft') to set default value if not provided Block syntaxt is also possible CallableDateTime = Types::DateTime.default { } so CallableDateTime[] #=> #<DateTime: 2020-04-01>
  • | sum of valid types .optional is implemented with nil_or_string = Types::Nil | Types::String Hash schemas to define a type for a hash with a known set of keys. Add ? to mark optional. Other keys are ignored unless .strict option is used

user_hash = Types::Hash.schema(
  name: Types::String,
  age?: Types::Coercible::Integer

You can transform keys from string to symbols .with_key_transform(&:to_sym). Also use inheritance by calling .shema again

with_city = user_hash.schema(city: Types::String)
with_city[name: 'Me', city: 'NS')

You can also transform types with a block.

Use enum similar to Rails enum but this will raise error if not in list "a" violates constraints (included_in?(["text_open", "text_time"], "a") failed) so it will not coerce.

    property :kind, type: Types::String.enum(Element.kinds)

Enable maybe extension gem install dry-monads and irb

require 'dry-types'
module Types
  include Dry.Types()

x = Types::Maybe::Strict::Integer[nil]
=> None
x = Types::Maybe::Strict::Integer[1]
=> Some(1)

Dry monads

Dry schema is used by dry validation and provides structural validation (validate key presence), pre-coercion filtering, so focus is in structure validation and data types validation. After gem install dry-schema try in irb

require 'dry/schema'

UserSchema = Dry::Schema.Params do
  required(:age).maybe(:integer, gt?: 18)
  required(:address).hash do

result = name: 'Jane', age: nil, address: {city: 'NYC'}
result.succees? # => false
result.errors.to_hash => {:address=>{:street=>["is missing"]}}

Three things are happened:

  • input keys are coerced to symbols using schema’s key map
  • input values are coerced based on type specs (for .maybe(:array) empty string '' will be coerced to empty array []).
  • input keys and values are validated using defined schema rules

Basic macros

  • value is used to provide a list of all predicates that will be AND-ed required(:age).velue(:integer, gt?: 18) is required(:age) { int? & gt?(18) }
  • filled means that value is non-nil and in case of String, Hash and Array, is it also not .empty?
    required(:tags).filled(:array) # is array? * filled?
  • maybe when value can be nil (optional means that key can be ommited, but maybe requires key, but value can be nil)
    required(:age).maybe(:integer) # !nil?.then(int?)
  • hash value is expected to be a hash
    required(:tags).hash do # hash? & filled? & schema { required(:name).filled }

    You can use that to define Nested data. It is the same as call value(:hash). If nested hash is not required simply use required(:tags).maybe(:hash?).

  • schema similar to .hash but without hash? predicate
  • array is used to apply predicate to every element in value that is expected to be an array
    # elements are string
    # elements are hashes
    requred(:tags).array(:hash) do

    If you need to apply predicates min_size?: 1 use full form

    required(:tags).value(:array, min_size?: 1) do
      hash do
  • each is similary to .array but does not prepent array? predicate

When first argument is a symbol without question mark, it will be set as the type. Inside Dry::Schema::Params for :integer will be resolved this class Dry::Schema::Params::Integer. Array member type

required(:number).value(array[:integer], size?: 3)

Built-in predicates (simpler than those used in dry-validation v0.13)

  • nil? check if key value is nil (similar :false? :true?)
  • type? check if class is equal given class (shorthand str?, int?, float?, decimal?, bool?, time?, date_time?, array?, hash?)
    required(:sample).value(type?: Integer)
  • empty? check that either string, array or hash is empty
  • filled? check that either value is not-nil and in case of string, array or hash, is non-empty
  • eql? check if value is equal (similar: lt?, lteq?, gt?, gteq?, size?, max_size?, min_size?, bytesize?
    required(:sample).value(eql?: 1)
  • format? check that string matches a given regular expression
    required(:sample).value(format: /*a/
  • included_in? value is incuded in given array (similar excluded_from?)
    required(:sample).value(included_in?: [1,2])

Note that you can use predicate logic so invalid state will not crash one of your rules

schema = Dry::Schema.Params do
  required(:age).value(gt?: 18)

# this will raise exception
schema.(age: 'a') # ArgumentError (comparison of String with 18 failed)

schema_with_integer = Dry::Schema.Params do
  required(:age) { int? & gt?(18) }
schema_with_integer.(age: 'a')
 => #<Dry::Schema::Result{:age=>"a"} errors={:age=>["must be an integer"]}>

You can define your base schema class that contains shared rules

class AppSchema < Dry::Schema::Params
  define do
    config.messages.load_paths << '/my/app/config/locales/en.yml'
    config.messages.backend = :i18n

    # define common rules, if any

# now you can build other schemas on top of the base one:
class MySchema < AppSchema
  # define your rules

my_schema =

Optional keys means that key can be ommited, but if present we can apply rules it was extracted from older dry-validation optional(:age).filled here optional means that KEY age is not required, but when it is present it should be filled (not a nil, empty sting, array or hash) and has to be int? and gt?: 18

schema = Dry::Schema.Params do
  optional(:age).filled(:integer, gt?: 18)

Optional values means that value can be nil, so in this example age: nil is valid, as age: 19 is also valid. Can not be empty string, maybe only permits nil, but if you use type: Types::Params::Integer than coercion will make empty string to nil .

  optional(:age).maybe(:integer, gt?: 18)

You can filter inputs before coercion. This is used in checking if date is in right format.

  required(:birthday).filter(format?: /\d{4}-\d{2}-\d{2}/).value(:date)

You can also use Abstract syntax tree AST of the schema schema.to_ast and its schema.key_map or

To create custom type StrippedString and used it as :stripped_string

Dry validations Main reason of implementing dry-validations instead activerecord and activemodel validations, is that it does not override methods like :errors or :attributes (so you can have this names as colomn names). Validation is performed on input data (not on the activerecord model). Another reason is that validations are changing (for example adding validate :phone, presence: true)) and if you forgot to migrate data you end up in bugs when old objects are instantiated (in invalid state). Another is separation of structural validation and type safety (dr-schema) and on another side, domain validation rules. So simple first step validation are inside params, schema, json (they have different coercing rules) block but something

Validations are expressed using contract object that are defined by schema with basic type check and any additional rules that should be applied (it will be applied after schema is verified). Main value is separation of schema from domain validation logic which can be simplified since we do not need to care about types (dry-schema will sanitize, coerce and type check). After installing gem install dry-validation in irb you can

require 'dry-validation'

class NewUserContract < Dry::Validation::Contract
  params do

  rule(:age) do
    key.failure('must be greater than 18') if value <= 18
contract = '[email protected]', age: '17')
=> #<Dry::Validation::Result{:email=>"[email protected],asd", :age=>17} errors={:age=>["must be greater than 18"]}>

For defining dry-schema you can use schema or params (better for HTTP since it will coerce string to int). You can reuse schema

AddressSchema = Dry::Schema.Params do

ContactSchema = Dry::Schema.Params do

class NewUserContract < Dry::Validation::Contract
  params(AddressSchema, ContactSchema) do

Use virtual attribute with default value to validate associations

  property :not_used, virtual: true, default: true

  validation do
    configure with: { form: true } do
      def not_used?(_value)
        # we can not proceed if is used
    required(:not_used).filled :not_used?

required(:f).value :none? check that value is nil (note you can not use maybe? with none? predicate)

What you need to know for using Trailblazer

  • you need to learn DSL which change from version to version. You lose if you do not follow up documentation.
  • errors for trailblazer because of eager loading or something, restart server helps
    uninitialized constant #<Class:0x00007f6b986a5da0>::Index Did you mean? Bindex` .
    NameError (uninitialized constant #<Class:0x000055ab8fc752f0>::Update

    This can be solved by writing module [ConceptName}::Operation and in next line class Update < Trailblazer::Operation so it is properly loaded with rails autoloader, you need to disable trailblazer loader

    # config/initializers/trailblazer.rb
    YourApp::Application.config.trailblazer.enable_loader = false
  • can not use form partials, for example if you want to render another partial

Graph modeling 2.1

Instead of subclassing we use compositions. Inside module we call module_function so it is more readable and allows to use method to attach a module method to a step

module Memo::Update
  extend Trailbazer::Activity::Railway()


  def find_model(ctx, id:, **)
    ctx[:model] = Memo.find_by id: id

  step method(:find_model)

Invoke like

ctx = { id: 1, params: { body: 'Awesome' } }
event, (ctx, *) = Memo::Update.( [ctx, {}] )
pp ctx #=>
{ id: 1,
  params: { body: 'Awesome' },
  model: #<struct Test::Memo body=nil>,
  errors: 'body not long enough',


For Path, it is automatically wired :success / Activity::Right semantic / signal. You need to manually wire other outputs

class Create < Trailblazer::Activity::Path
  step :validate, Output(Activity::Left, :failure) => End(:invalid)
  step :create
  # ...

You can jump to End(:db_error) or back to track Track(:success). When you use you get semantic of last

ctx = {params: nil}
signal, (ctx, flow_options) = Memo::Create.([ctx, {}])

puts signal #=> #<Trailblazer::Activity::End semantic=:invalid>

For Railway there are two tracks

class Create < Trailblazer::Activity::Railway
  step :validate
  fail :log_error, Output(:success) => Track(:success)
  step :create
  # ...

You can use pass :create so both success and failure will end in End success

You need sometime to access global constant, for example in operation

# app/concepts/element/clone.rb
class Element::Create < Trailblazer::Operation
  step a
  step Model(::Element, :new)

  def a(ctx)
    Element # This is Trailblazer::Operation::Element, not Element model