Use when writing Rails controllers or implementing state changes - enforces resource extraction, thin controllers delegating to models, params.expect, and controller concerns for scoping
From vanilla-railsnpx claudepluginhub zemptime/zemptime-marketplace --plugin vanilla-railsThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
State changes are resources. Every state change becomes its own resource controller with CRUD operations.
# Bad - custom actions
resources :cards do
post :close
post :archive
end
# Good - state as resource
resources :cards do
resource :closure, only: [:create, :destroy]
resource :archival, only: [:create, :destroy]
end
See vanilla-rails-routing for route structure, nesting, and directory mapping.
| State Change | Resource | create = | destroy = |
|---|---|---|---|
| Close/Reopen | closure | close | reopen |
| Archive/Unarchive | archival | archive | unarchive |
| Pin/Unpin | pin | pin | unpin |
| Publish/Unpublish | publication | publish | unpublish |
| Assign/Unassign | assignment | assign | unassign |
| Follow/Unfollow | subscription | subscribe | unsubscribe |
Controllers delegate to intention-revealing model methods. No business logic in controllers.
# Bad - logic in controller
class Cards::ArchivalsController < ApplicationController
def create
@card.update(archived: true)
end
end
# Good - delegate to model
class Cards::ArchivalsController < ApplicationController
include CardScoped
def create
@card.archive
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@card.unarchive
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
Extract parent-finding into concerns. Name describes what's scoped:
# app/controllers/concerns/card_scoped.rb
module CardScoped
extend ActiveSupport::Concern
included do
before_action :set_card
end
private
def set_card
@card = Card.find(params[:card_id])
end
end
# app/controllers/concerns/board_scoped.rb
module BoardScoped
extend ActiveSupport::Concern
included do
before_action :set_board
end
private
def set_board
@board = Current.account.boards.find(params[:board_id])
end
end
Common concerns:
| Concern | Sets | Used by |
|---|---|---|
BoardScoped | @board | All controllers under boards/ |
CardScoped | @card | All controllers under cards/ |
Authenticated | session check | All controllers needing auth |
Use params.expect() instead of params.require().permit():
# Bad
def card_params
params.require(:card).permit(:title, :description)
end
# Good
def card_params
params.expect(card: [:title, :description])
end
State resources need a table tracking who/when:
create_table :closures, id: :uuid do |t|
t.uuid :card_id, null: false
t.uuid :user_id
t.timestamps
end
add_index :closures, :card_id, unique: true
| Red flag | Fix |
|---|---|
post :close, patch :activate | Extract resource |
| Business logic in controller | Move to model |
params.require().permit() | Use params.expect() |
before_action duplicated across controllers | Extract scoping concern |
| Controller > 30 lines per action | Delegate more to model |
params.expect() for strong parameters