thibautbaissac/rails_ai_agents/state-records

Claude Code and agent skill by thibautbaissac/rails_ai_agents

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

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

Install

npx vskill@latest install thibautbaissac/rails_ai_agents/state-records

SKILL.md

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:

ruby
class Closure < ApplicationRecord
belongs_to :account, default: -> { card.account }
belongs_to :card, touch: true
belongs_to :user, optional: true
validates :card, uniqueness: true
after_create_commit :notify_watchers
after_destroy_commit :notify_watchers
private
def notify_watchers
card.notify_watchers_later
end
end

State concern template

Every state concern follows this structure:

ruby
module Card::Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
scope :open, -> { where.missing(:closure) }
scope :closed, -> { joins(:closure) }
end
def close(user: Current.user)
create_closure!(user: user)
track_event "card_closed", user: user
end
def reopen
closure&.destroy!
track_event "card_reopened"
end
def closed?
closure.present?
end
def open?
!closed?
end
def closed_at
closure&.created_at
end
def closed_by
closure&.user
end
end

State record with metadata

When state needs additional data (secure tokens, descriptions):

ruby
class Board::Publication < ApplicationRecord
belongs_to :account, default: -> { board.account }
belongs_to :board, touch: true
has_secure_token :key
validates :board, uniqueness: true
def public_url
Rails.application.routes.url_helpers.public_board_url(key)
end
end
module Board::Publishable
extend ActiveSupport::Concern
included do
has_one :publication, dependent: :destroy
scope :published, -> { joins(:publication) }
scope :private, -> { where.missing(:publication) }
end
def publish(description: nil)
create_publication!(description: description)
track_event "board_published"
end
def unpublish
publication&.destroy!
track_event "board_unpublished"
end
def published?
publication.present?
end
def public_url
publication&.public_url
end
end

Query patterns with state records

ruby
# Finding by state: positive uses joins, negative uses where.missing
Card.open # where.missing(:closure)
Card.closed # joins(:closure)
Board.published # joins(:publication)
Card.golden # joins(:goldness)
# Complex combinations
scope :actionable, -> {
where.missing(:closure).where.missing(:not_now).where.missing(:archival)
}
# Sorting by state
scope :with_golden_first, -> {
left_outer_joins(:goldness)
.select("cards.*", "card_goldnesses.created_at as golden_at")
.order(Arel.sql("golden_at IS NULL, golden_at DESC"))
}
# Filtering by actor
scope :closed_by, ->(user) { joins(:closure).where(closures: { user: user }) }

Controller patterns

State changes map to singular resources with create/destroy:

ruby
# config/routes.rb
resources :cards do
resource :closure, only: [:create, :destroy], module: :cards
resource :goldness, only: [:create, :destroy], module: :cards
resource :not_now, only: [:create, :destroy], module: :cards
end
# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close(user: Current.user)
render_card_replacement
end
def destroy
@card.reopen
render_card_replacement
end
end

View patterns

erb
<%# Toggle button %>
<%= button_to card_goldness_path(card),
method: card.golden? ? :delete : :post,
data: { turbo_frame: dom_id(card) } do %>
<%= card.golden? ? "Ungild" : "Gild" %>
<% end %>
<%# State badge %>
<% if card.closed? %>
<span class="badge badge--closed">
Closed <%= time_ago_in_words(card.closed_at) %> ago
<% if card.closed_by %>by <%= card.closed_by.name %><% end %>
</span>
<% end %>

When to use state records vs booleans

Use state records when:
  • You need to know when state changed
  • You need to know who changed it
  • You might store metadata (reason, notes)
  • State changes are important business events
  • You need queries like "recently closed" or "closed by X"
Use booleans when:
  • State is purely technical (cached, processed)
  • Timestamp/actor don't matter
  • Performance is critical (millions of rows, frequent updates)
  • State changes are not business events
Quick reference:
  • State records: closed, published, archived, suspended, verified, pinned, golden, postponed
  • Booleans: admin, cached, processed, visible

See references/state-record-examples.md for complete examples of each state type.

Migration from boolean to state record

  1. Create state record model + migration
  2. Backfill existing data
  3. Update model code to use concern
  4. Remove boolean column (after verification)

Boundaries

  • Always: Create state record for business-meaningful states, track who and when, use where.missing for negative scopes, add unique index on parent_id, touch parent record, write tests for state transitions
  • Ask first: Before using boolean columns for business state, before adding complex metadata (might need separate model)
  • Never: Use booleans for important business state, skip who/when tracking, create multiple state records per parent (use has_one with unique index), skip event tracking for state changes

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
589
Repository forks
67
7-day trend score
32

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/thibautbaissac/rails_ai_agents/state-records/badge)](https://verified-skill.com/skills/thibautbaissac/rails_ai_agents/state-records)

Something wrong with this skill or its page? Tell us.