FlowState: Model complex workflows with Rails
I’ve been working on flow_state, a small gem for modelling, orchestrating and tracing multi‑step workflows natively with Rails.
Back story
Over the years I’ve built countless iterations on pipelines that:
- Fetch data from third-party APIs
- Blend and transform it (lately with generative AI)
- Persist the result in my own models
They always ended up as tangled callbacks or chains of jobs calling jobs, with retry logic scattered everywhere. When something failed, it was hard to trace what happened and where.
I wanted:
- A Rails-native orchestration layer on top of SolidQueue/ActiveJob
- No external dependencies like Temporal or Restate
- A clear audit trail of each state transition, whether it succeeded or failed
- A conventional and Rails-y way to declare states, transitions, guards and persisted artefacts
The core idea
With flow_state you:
- 
Define props 
 storing metadata required to initialise a workflow.prop :third_party_id, String
- 
Declare states state :pending state :syncing_api state :failed_api, error: true initial_state :pending
- 
Define every possible transition 
 transition!specifies which states can be transitioned from and to, and validates every move. It prevents race conditions by not allowing transitions between incompatible states.
- 
Define schemas for persisted artefacts 
 Artefacts are stored during one transition, for use in another.persist :third_party_response, Hash
- 
Apply guards 
 Block a transition unless a condition holds.
- 
Write your steps as explicit methods 
 No magic methods or heavy meta-programming.def start_api_fetch! transition!(from: :pending, to: :started_api_fetch) end def finish_api_fetch!(result) transition!(from: :started_api_fetch, to: :finished_api_fetch) { result } end def fail_api_fetch! transition!(from: :started_api_fetch, to: :failed_api_fetch) end
Example: synchronise song data
class SongSyncFlow < FlowState::Base
  prop :song_id, String
  state :pending
  state :picked
  state :syncing_api
  state :synced_api
  state :failed_api, error: true
  state :syncing_model
  state :synced_model
  state :failed_model, error: true
  state :completed
  initial_state :pending
  persist :api_response
  def pick!
    transition!(
      from: %i[pending completed failed_api failed_model],
      to:   :picked,
      after_transition: -> { enqueue_api_job }
    )
  end
  def finish_api!(response)
    transition!(
      from:    :syncing_api,
      to:      :synced_api,
      persists: :api_response,
      after_transition: -> { enqueue_model_job }
    ) { response }
  end
  def fail_api!
    transition!(from: :syncing_api, to: :failed_api)
  end
  # …and so on…
end- Your jobs simply call pick!,finish_api!, etc.
- Your transitions cause errors if your app attempts to transition from incompatible states, as modelled by you.
- Each call is locked, logged and validated.
- Guards stop you moving on until data is correct.
- Artefacts can be stored between transitions, for later reference.
Getting started
- Add to your Gemfile
bundle add 'flow_state'
- Run the installer and migrate
rails generate flow_state:install rails db:migrate
- Create your flows in app/flows, ie.SongSyncFlowinapp/flows/song_sync_flow.rb
I’m using it across two projects at the moment, and it’s made reasoning about complex stepped workflows considerably easier. It’s helped me design workflows that are resilient by default.
