State Records
Implements the state-as-records-not-booleans pattern for rich state tracking.
Install
npx skills add ThibautBaissac/rails_ai_agents --skill state-records -g -a claude-code -y- Repository
- ThibautBaissac/rails_ai_agents671
- Category
- Engineering
- Updated
- 5mo ago
- License
- MIT
- Context cost
- Mediumabout 1,792 tokens
- Runs scripts
- No, instructions only
What it does
Implements the state-as-records-not-booleans pattern for rich state tracking. Use when modeling state changes, replacing boolean flags with record-based state, or when user mentions state records, closures, publications, or toggling state. WHEN NOT: Technical flags like cached/processed (use booleans), concern extraction (use concern-patterns), general model work (use model-patterns).
State Records (37signals)
State as records, not booleans. Instead of closed: boolean, create a Closure record.
Project knowledge
Tech Stack: Rails 8.2 (edge), UUIDs everywhere, ActiveRecord associations Pattern: One state model per boolean you'd normally add Naming: Noun forms (Closure, Publication, Goldness, NotNow, Archival)
Commands:
bin/rails generate model Closure card:references:uuid user:references:uuid account:references:uuid
bin/rails db:migrate
bin/rails console # Test: Card.open.count
bin/rails test test/models/
Why state records over booleans
Boolean columns give you:
- Current state (open/closed)
State records give you:
- Current state (
closure.present?) - When it changed (
closure.created_at) - Who changed it (
closure.user) - Why it changed (
closure.reason) - Change history (via events)
The pattern
Boolean approach (avoid for business state):
# BAD
class Card < ApplicationRecord
def close
update!(closed: true, closed_at: Time.current)
end
scope :open, -> { where(closed: false) }
end
State record approach:
# GOOD
class Closure < ApplicationRecord
# touch: true ensures the parent's updated_at changes when state changes,
# which drives cache invalidation (Russian doll caching, ETags, etc.)
belongs_to :card, touch: true
belongs_to :user, optional: true
belongs_to :account, default: -> { card.account }
validates :card, uniqueness: true
end
class Card < ApplicationRecord
has_one :closure, dependent: :destroy
def close(user: Current.user)
create_closure!(user: user)
end
def reopen
closure&.destroy!
end
def closed?
closure.present?
end
scope :open, -> { where.missing(:closure) }
scope :closed, -> { joins(:closure) }
end
State record model template
Every state record model follows this structure:
