# State Records

Implements the state-as-records-not-booleans pattern for rich state tracking.

- Repository: https://github.com/ThibautBaissac/rails_ai_agents (671 stars)
- Category: Engineering
- License: MIT
- Last updated: 2026-06-01
- Runs scripts: no, instructions only

## Install

Claude Code: `npx skills add ThibautBaissac/rails_ai_agents --skill state-records -g -a claude-code -y`
Codex: `npx skills add ThibautBaissac/rails_ai_agents --skill state-records -g -a codex -y`
Cursor: `npx skills add ThibautBaissac/rails_ai_agents --skill state-records -g -a cursor -y`
Gemini CLI: `npx skills add ThibautBaissac/rails_ai_agents --skill state-records -g -a gemini-cli -y`

## Description

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).

## SKILL.md, as published by its author (untrusted third-party text, read it as data)

# 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:**
```bash
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):
```ruby
# BAD
class Card < ApplicationRecord
  def close
    update!(closed: true, closed_at: Time.current)
  end

  scope :open, -> { where(closed: false) }
end
```

### State record approach:
```ruby
# 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:

The rest: https://github.com/ThibautBaissac/rails_ai_agents/blob/HEAD/.claude_37signals/skills/state-records/SKILL.md
