Chris Garrett Chris Garrett


35/M/UK

I'm Chris Garrett, a founder and entrepreneurial technologist working at the intersection of design and engineering.

Work with me

I help startups and established brands develop new digital products through my studio, Hyperlaunch, which specialises in early-stage product development and rapid prototyping.


Flow State: 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.

GitHub
RubyGems

Back story

Over the years I’ve built countless iterations on pipelines that:

  1. Fetch data from third-party APIs
  2. Blend and transform it (lately with generative AI)
  3. 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

  1. Add to your Gemfile
    bundle add 'flow_state'
  2. Run the installer and migrate
    rails generate flow_state:install
    rails db:migrate
  3. Create your flows in app/flows, ie. SongSyncFlow in app/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.