plugins/lisa-rails/skills/active-record-model-best-practices/SKILL.md
Best practices for Ruby on Rails models, splitting code into well-organized, maintainable code. Use when a model exceeds ~100 lines, has mixed responsibilities, or when the user asks to refactor, extract, clean up, or organize a Rails model. Applies patterns: concerns, service objects, query objects, form objects, and value objects.
npx skillsauth add codyswanngt/lisa active-record-model-best-practicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
When refactoring a Rails model, analyze the file and extract code into the appropriate pattern based on what the code does. The model itself should only contain associations, enums, basic validations, and concern includes.
Read the model file and classify each block of code:
| Code type | Extract to | Location |
|---|---|---|
| Related scopes + simple methods sharing a theme | Concern | app/models/concerns/ |
| Business logic, multi-step operations, callbacks with side effects | Service object | app/services/ |
| Complex queries, multi-join scopes, reporting queries | Query object | app/queries/ |
| Context-specific validations (e.g. registration vs admin update) | Form object | app/forms/ |
| Domain concepts beyond a primitive (money, coordinates, scores) | Value object | app/models/ |
| Associations, enums, core validations, simple scopes | Keep on model | — |
Use for grouping related scopes, validations, callbacks, and simple instance methods that share a single theme. Name the concern after the capability it provides.
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(query) { where("name ILIKE ?", "%#{query}%") }
end
def matching_terms(query)
name.scan(/#{Regexp.escape(query)}/i)
end
end
Use for business logic, orchestration of multiple models, and anything triggered by a user action that involves more than a simple CRUD operation. Follow the single-responsibility principle — one service, one operation.
# app/services/players/calculate_stats.rb
module Players
class CalculateStats
def initialize(player)
@player = player
end
def call
# complex logic here
end
end
end
Conventions:
Players::CalculateStatscallinitializeUse for complex database queries that involve joins, subqueries, CTEs, or multi-condition filtering that would clutter a model with scopes.
# app/queries/players/free_agent_query.rb
module Players
class FreeAgentQuery
def initialize(relation = Player.all)
@relation = relation
end
def call(filters = {})
@relation
.where(contract_status: :expired)
.where("age < ?", filters[:max_age])
.joins(:stats)
.order(war: :desc)
end
end
end
Conventions:
initialize (default to Model.all)callUse when validations only apply in specific contexts, or when a form spans multiple models.
# app/forms/player_registration_form.rb
class PlayerRegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :email, :string
attribute :team_id, :integer
attribute :position, :string
validates :name, :email, :position, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
def save
return false unless valid?
Player.create!(attributes)
end
end
Use for domain concepts that deserve their own identity beyond a raw primitive.
# app/models/batting_average.rb
class BattingAverage
include Comparable
def initialize(hits, at_bats)
@hits = hits
@at_bats = at_bats
end
def value
return 0.0 if @at_bats.zero?
(@hits.to_f / @at_bats).round(3)
end
def elite?
value >= 0.300
end
def <=>(other)
value <=> other.value
end
end
development
Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything.
tools
Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv.
tools
Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use.
tools
Route one unit of work to a remote execution surface. Reads the executionEnv parameter (local by default, codex-cloud or claude-web today), verifies the environment is provisioned and bound to this repository, submits a thin skill invocation, records the task identifier to .lisa/remote-dispatch.json, and exits without polling. Routing only — the remote runs the identical skill from the identical repository. Composable and inline: other skills invoke it via the Skill tool rather than users calling it directly.