Operation 2.1 API

Last updated 23 November 2018 trailblazer-operation v2.1



  def a(x=1)



Circuit Interface

Activities and all tasks (or “steps”) are required to expose a circuit interface. This is the low-level interface. Most of the times it is hidden behind the task interface that you’re probably used to from your operations when using step. Under the hood, however, all callable circuit elements operate through that very interface.

The circuit interface consists of three things.

  • A circuit element has to expose a call method.
  • The signature of the call method is call((ctx, flow_options), **circuit_options).
  • Return value of the call method is an array of format [signal, [new_ctx, new_flow_options]].

Do not fear those syntactical finesses unfamiliar to you, young padawan.

module Create
  extend Trailblazer::Activity::Railway()

  def validate((ctx, flow_options), **circuit_options)
    # ...
    return signal, [ctx, flow_options]

  step task: method(:validate)

Both the Create activity itself and the validate step expose the circuit interface. Note that the :task option for step configures this element as a low-level circuit interface, or in other words, it will skip the wrapping with the task interface.

Maybe it makes more sense now when you see how an activity is called manually? Here’s how to invoke Create.

ctx          = {name: "Face to Face"}
flow_options = {}

signal, (ctx, flow_options) = Create.([ctx, flow_options], {})

signal #=> #<Trailblazer::Activity::End semantic=:success>
ctx    #=> {:name=>\"Face to Face\", :validate_outcome=>true}

Note that both ctx and flow_options can be just anything. Per convention, they respond to a hash interface, but theoretically it’s up to you how your network of activities and tasks communicates.

Check the implementation of validate to understand how you return a different signal or a changed ctx.

def validate((ctx, flow_options), **circuit_options)
  is_valid = ctx[:name].nil? ? false : true

  ctx    = ctx.merge(validate_outcome: is_valid) # you can change ctx
  signal = is_valid ? Trailblazer::Activity::Right : Trailblazer::Activity::Left

  return signal, [ctx, flow_options]

Make sure to always stick to the return signature on the circuit interface level.


The circuit interface is a bit more clumsy but it gives you unlimited power over the way the activity will be run. And trust us, we’ve been playing with different APIs for two years and this was the easiest and fastest outcome.

def validate((ctx, flow_options), **circuit_options)
  # ...
  return signal, [ctx, flow_options]

The alienating signature uses Ruby’s decomposition feature. This only works because the first argument for call is actually an array.

Using this interface empowers you to fully take control of the flow™.

  • You can return any signal you want, not only the binary style in steps.
  • If needed, the ctx object might be mutated or, better, replaced and a new version returned. This is the place where you’d start implementing an immutable version of Trailblazer’s ctx, for instance.
  • Advanced features like tracing, input/output filters or type checking leverage the framework argument flow_options, which will be passed onwards through the entire activities flow. Know what you’re doing when using flow_options and always return it even if you’re not changing it.
  • The circuit_options is another framework argument needed to control the start task and more. It is immutable and you don’t have to return it. The same circuit_options are guaranteed to be passed to all invoked tasks in one activity.

Since in 99% the circuit_options are irrelevant, it’s nicer and faster to discard them instantly.

def validate((ctx, flow_options), *)
  # ...

Use the lonely * squat asterisk to do so.

Trailblazer in Rails

Trailblazer runs with any Ruby web framework. However, if you’re using Rails, you’re lucky since we provide convenient glue code in the trailblazer-rails gem.

gem "trailblazer-rails"

todo: add versioning information


The trailblazer-loader gem implements a very simple way to load all files in your concepts directory in a heuristically meaningful order. It can be used in any environment.

Loader with Rails

The trailblazer-loader gem comes pre-bundled with trailblazer-rails for historical reasons: in the early days of Trailblazer, the conventional file name concepts/product/operation/create.rb didn’t match the short operation name, such as Product::Create.

The trailblazer-loader gem’s duty was to load all concept files without using Rails’ autoloader, overcoming the latter’s conventions.

Over the years, and with the emerge of controller helpers or our workflow engine calling operations for you, the class name of an operation more and becomes a thing not to worry about.

Many projects use Trailblazer along with the Rails naming convention now. This means you can disable the loader gem, and benefit from Rails auto-magic behavior such as faster loading in the “correct” order, reloading and all the flaws that come with this non-deterministic behavior.

As a first step, add Operation to your operation’s class name, matching the Rails naming convention.

# app/concepts/product/operation/create.rb

module Product::Operation
  class Create < Trailblazer::Operation
    # ...

It’s a Trailblazer convention to put [ConceptName]::Operation in one line: it will force Rails to load the concept name constant, so you don’t have to reopen the class yourself.

This will result in a class name Product::Operation::Create.

Next, disable the loader gem, in config/initializers/trailblazer.rb.

# config/initializers/trailblazer.rb

YourApp::Application.config.trailblazer.enable_loader = false

Trailblazer files will now be loaded by Rails - you need to follow the Rails autoloading file naming from here on, and things should run smoothly. A nice side-effect here is that in bigger projects (with hundreds of operations), the start-up time in development accelerates significantly.