plutonium-behavior
Single source of truth for Plutonium resource behavior, covering controllers, policies, and interactions.
Install
mkdir -p .claude/skills/plutonium-behavior && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14694" && unzip -o skill.zip -d .claude/skills/plutonium-behavior && rm skill.zipInstalls to .claude/skills/plutonium-behavior
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Use BEFORE writing or overriding a Plutonium controller, policy, or interaction class. Covers controller hooks, policy methods, permitted attributes, relation_scope, interaction structure, outcomes, and chaining. The single source for "how does this resource actually do things".Key capabilities
- →Define authorization rules for actions
- →Implement business logic for resource modifications
- →Customize controller behavior with hooks
- →Specify permitted attributes for resource updates
- →Handle validation errors from database operations
How it works
This skill guides the implementation of Plutonium behavior layers by defining where authorization, business logic, and controller customizations should reside within controllers, policies, and interaction classes.
Inputs & outputs
When to use plutonium-behavior
- →Write a new interaction class
- →Update controller policy methods
- →Define permitted attributes for a resource
About this skill
Plutonium Behavior — Controllers, Policies, Interactions
The behavior layer is intentionally thin: controllers route, policies authorize, interactions act. Registering an action and rendering it lives in [[plutonium-resource]] — this skill covers how to write the controller hook, policy method, or interaction class behind it.
For tenant-scoped relation_scope and entity scoping, load [[plutonium-tenancy]].
🚨 Critical (read first)
- Use generators.
pu:res:scaffoldcreates the base trio (controller/policy/interaction-base);pu:res:conncreates portal-specific versions. Never hand-write them. - Don't override CRUD actions. Use hooks (
resource_params,redirect_url_after_submit, presentation hooks). Overridingcreate/updateusually breaks authorization, params filtering, or both. create?andread?default tofalse. Always override them explicitly. Derived methods (update?,show?, etc.) inherit automatically.permitted_attributes_for_*must be explicit in production. Dev auto-detection works; production raises.ActiveRecord::RecordInvalidis NOT rescued automatically in interactions. Always rescue when usingcreate!/update!/save!, returnfailed(e.record.errors).- Return
succeed(...)orfailed(...)fromexecute— the controller can't tell what happened otherwise. - An interaction is a presentation object (it can only be built with a
view_context). Logic may start inexecute; the second caller — a job, an API controller, a rake task, the console — is the trigger to move it onto the model. Don't pre-extract, and don't invent a service layer. See Part 3 › Where the logic goes. - Redirect is automatic on success — only use
with_redirect_responsefor a different destination. relation_scopemust end up callingdefault_relation_scope(relation)somewhere in the chain. Prefer calling it explicitly.superworks when extending a parent policy (e.g., a package base) that itself calls it. See [[plutonium-tenancy]].- For
has_centsfields, use the virtual name (:price), not:price_centsinpermitted_attributes_for_*. - Custom action ⇒ policy method.
action :publishneedsdef publish?on the policy (undefined methods returnfalse). - Named custom routes. When adding custom routes, always pass
as:soresource_url_forcan build URLs.
🛑 Before you write behavior: place it in the right layer (ASK — don't infer)
"Make X happen" doesn't say where X lives. Put it in the wrong layer and you get authorization that doesn't authorize, a 500 on the happy path, or a CRUD override that breaks params/auth. First place the requirement, then confirm names against the real code (next section):
| The requirement (in plain words) | Goes in | NOT in |
|---|---|---|
| "only <role/owner> may do X" — who is allowed | Policy def x? | a condition: proc — that only hides the button; the route stays live and callable |
| "there's a button that does X" — the trigger | Interaction (+ action in the definition) | a hand-written controller action; an override of create/update |
| "doing X changes state / sends mail / charges a card" — the work | a named model method the interaction calls (post.publish!) — inline in execute is fine while the button is the only caller | a service-object layer; three chained interactions |
| "after create/update go to Y" · "munge a param" · "reshape the index query" | Controller hook (redirect_url_after_submit, resource_params, filtered_resource_collection) | overriding create/update/index |
| "which fields are visible / editable" | Policy permitted_attributes_for_* | the definition — that only controls how a field renders |
Then resolve the specifics:
- A custom action needs BOTH: an interaction (the work) and a policy
def <action>?(the authorization). Miss the policy method ⇒ the action silently returnsfalse(dead button). Put the role check incondition:⇒ it isn't enforced — a direct POST still runs. create?/read?default tofalse— override explicitly; derived methods (update?/show?/…) inherit.- Any
create!/update!/save!inexecute⇒ rescueActiveRecord::RecordInvalid→failed(e.record.errors). Not auto-rescued — otherwise a validation failure 500s. has_cents⇒ permit:price, never:price_cents.- New vs editing — never re-scaffold a controller/policy/interaction that's been customized.
Never ship a guessed role method, column, enum value, or association as applied code. user.finance?, record.status_approved?, expense.submitted_by either exist in the app or they don't — confirm them before writing, don't assume. Fall back to AskUserQuestion only for genuine product choices (what the rule should be), never for facts you can read.
✅ Before you edit: verify the ground truth (CHECK — read it, don't ask for it)
You have file access — inspect; don't ask the user to describe their own app.
| Check | How | Why it matters |
|---|---|---|
| File already customized | Read app/policies/<x>_policy.rb, the controller, app/interactions/* | Edit incrementally — re-scaffolding clobbers customizations |
| The role/method you authorize on exists | grep the user model for def finance? / enum :role / has_role? | user.finance? 500s (or is silently false) if absent |
| The columns/enum your interaction writes | Read the model + db/schema.rb for the enum value, approved_by/approved_at, the submitter assoc | update!(status: :approved) raises if the value/column is missing |
| Action not already wired | grep the definition for action :<x>; grep the policy for def <x>? | Avoids duplicate or dead actions |
| Cross-resource access | Use authorized_resource_scope / allowed_to?, never raw where/find | Raw queries bypass the other resource's tenancy + visibility |
Inspect with your own tools before proposing code.
🛠 Use the generator — and know what's hand-authored
| Task | How | Verify first |
|---|---|---|
| Base trio (controller + policy + interaction-base) | pu:res:scaffold | New resource |
| Portal-specific controller/policy | pu:res:conn … --dest=portal | Resource exists |
| A custom-action interaction | Hand-author in app/interactions/<name>_interaction.rb (subclass ResourceInteraction) — there is NO pu:res:interaction generator; don't invent one | — |
| Edit an existing customized policy/controller/interaction | Hand-edit the file | It was already generated — re-scaffolding clobbers it |
Part 1 — Controllers
Plutonium controllers ship full CRUD out of the box; nearly all customization lives in definitions / policies / interactions. The controller stays thin.
Base classes
# app/controllers/resource_controller.rb (installed once)
class ResourceController < ApplicationController
include Plutonium::Resource::Controller
end
# app/controllers/posts_controller.rb (per resource, generated by pu:res:scaffold)
class PostsController < ::ResourceController
# Empty — all CRUD inherited
end
What you get for free
| Action | Route | Purpose |
|---|---|---|
index | GET /posts | List with pagination, search, filters, sorting |
show | GET /posts/:id | Display single record |
new | GET /posts/new | Form |
create | POST /posts | Create |
edit | GET /posts/:id/edit | Form |
update | PATCH /posts/:id | Update |
destroy | DELETE /posts/:id | Delete |
Plus interactive-action routes for every action declared in the definition.
Where customization belongs
| Concern | Lives in |
|---|---|
| Field rendering (inputs, displays, columns) | Definition |
| Search, filters, scopes, sorting | Definition |
| Custom operations (publish, archive, import) — the button | Interaction (+ action in definition) |
| The operation itself, once a job/API/task also needs it | The model (post.publish!) — see Part 3 › Where the logic goes |
| Authorization rules | Policy |
| Form/show/page chrome | Definition (custom page classes) |
| Custom redirect logic | Controller hook |
| Param munging | Controller hook |
| Custom index query shape | Controller hook |
| Presentation of parent/entity fields | Controller hook |
Override hooks
All hooks are private methods. Override only the ones you need.
Redirect hooks
class PostsController < ::ResourceController
private
# Where to go after create/update: "show" (default), "edit", "new", "index"
def preferred_action_after_submit = "edit"
# Custom URL after create/update (overrides preferred_action_after_submit)
def redirect_url_after_submit = posts_path
# Custom URL after destroy
def redirect_url_after_destroy = posts_path
end
Parameter hook
def resource_params
params = super
params[:tags] = params[:tags].split(",") if params[:tags].is_a?(String)
params
end
Index query hook
def filtered_resource_collection
base = current_authorized_scope
base = base.featured if params[:featured]
current_query_object.apply(base, raw_resource_query_params)
end
Don't add eager loading unprompted. Which associations a page renders is decided by the definition, so an includes list written now is a guess that goes stale when a column is added. Adding one is a performance change the user didn't ask for.
When a user actually reports a slow index or an N+1: suggest goldiloader first — it eager-loads on traversal, so it tracks whatever the definition renders and needs no list to maintain. Only hand-write def filtered_resource_collection = super.includes(...) if they decline the gem, and use the policy's relation_scope instead when the association is also read on show/export/typeahead. Full detail: [Guides › Perfor
Content truncated.
When not to use it
- →When the task is to register an action or render it
- →When the task involves tenant-scoped relation_scope or entity scoping
- →When the task is about custom interaction form templates or page classes
Limitations
- →Does not cover registering actions or rendering them.
- →Does not cover tenant-scoped relation_scope or entity scoping.
- →Does not cover custom interaction form templates or page classes.
How it compares
This skill provides a structured framework for implementing application behavior, separating concerns into policies for authorization, interactions for actions, and controllers for routing, unlike a monolithic approach.
Compared to similar skills
plutonium-behavior side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| plutonium-behavior (this skill) | 0 | 1mo | Review | Advanced |
| architecture-patterns | 55 | 2mo | No flags | Advanced |
| kotlin-multiplatform | 32 | 3mo | Review | Advanced |
| nodejs-best-practices | 28 | 6mo | No flags | Advanced |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
architecture-patterns
wshobson
Implement proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design. Use when architecting complex backend systems or refactoring existing applications for better maintainability.
kotlin-multiplatform
vitorpamplona
Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.
nodejs-best-practices
davila7
Node.js development principles and decision-making. Framework selection, async patterns, security, and architecture. Teaches thinking, not copying.
workflow-orchestration-patterns
wshobson
Design durable workflows with Temporal for distributed systems. Covers workflow vs activity separation, saga patterns, state management, and determinism constraints. Use when building long-running processes, distributed transactions, or microservice orchestration.
java-pro
sickn33
Master Java 21+ with modern features like virtual threads, pattern matching, and Spring Boot 3.x. Expert in the latest Java ecosystem including GraalVM, Project Loom, and cloud-native patterns. Use PROACTIVELY for Java development, microservices architecture, or performance optimization.
arm-cortex-expert
sickn33
Senior embedded software engineer specializing in firmware and driver development for ARM Cortex-M microcontrollers (Teensy, STM32, nRF52, SAMD). Decades of experience writing reliable, optimized, and maintainable embedded code with deep expertise in memory barriers, DMA/cache coherency, interrupt-driven I/O, and peripheral drivers.