| name | avo-controllers |
| description | Override the per-resource CRUD controller hooks Avo generates (redirect paths, flash messages, custom responses, save/destroy behavior) and safely extend Avo's private ApplicationController. Use when the user wants to redirect somewhere else after creating/updating/deleting in the admin, change the "successfully created/updated" flash message, soft-delete or archive instead of destroying, swap `@record.save!` for a service object, run code before every admin request, set a `Current` attribute or tenant per admin request, override `fill_record`, or fix `ActionDispatch::MissingController`. Covers `Avo::CoursesController < Avo::ResourcesController`, the `avo:controller` generator, `after_create_path`/`after_update_path`/`after_destroy_path`, `*_success_message`/`*_fail_message`, `create_/update_/destroy_success_action`/`*_fail_action`, `save_record_action`/`destroy_record_action`, and extending `Avo::ApplicationController` with a concern in `to_prepare`. |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"none — Community"} |
These instructions ship inside the avo gem this app has locked, so they describe the version you are actually running. Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
Avo Resource Controllers
Avo generates a Rails controller alongside every resource so each one plugs into Rails' REST architecture. Each generated controller inherits from Avo::ResourcesController, which inherits from Avo::BaseController — and that base is where every CRUD action (index, show, new, create, edit, update, destroy) lives:
class Avo::CoursesController < Avo::ResourcesController
end
Most of the time you never touch this file — resource-level config on app/avo/resources/<name>.rb does the job. Reach for the controller when you need granular per-action control: where the user lands after a save, what the flash says, how the response is rendered, or how the record is actually persisted or removed. You do that by overriding hook methods in the generated controller, the same way Devise lets you override after_sign_in_path_for. For app-wide behavior that isn't tied to one resource (a Current attribute, a tenant, a before_action on every admin request), you extend Avo::ApplicationController instead — but never by copying it (see below).
Docs
Authoritative docs — fetch on demand rather than guessing, and verify every method name against the docs or the app's installed Avo source before writing it:
When this applies
Explicit (Avo named): "override the Courses controller", "override after_create_path/after_update_path/after_destroy_path", "change create_success_message", "override save_record_action / destroy_record_action", "generate a controller with avo:controller", "set config.resource_parent_controller", "extend Avo::ApplicationController", "override fill_record".
Implicit (Rails/product-shaped, no mention of Avo): "redirect somewhere else after saving/creating/deleting in the admin", "send the user to the dashboard after they create a record", "change the 'successfully created' message", "soft-delete instead of destroying in the admin", "archive instead of delete", "run a service object when I save instead of save!", "run code before every admin request", "set a Current attribute on each admin request", "set the tenant / add multitenancy in the admin", "customize what happens after I save a record", "I get ActionDispatch::MissingController when I open a resource".
Workflow
1. Know which lever to pull
Before opening a controller, check whether a resource-level option already covers it — it's less code and survives upgrades better:
- Just want to land on or after a save? Set
self.after_create_path / self.after_update_path (:show | :edit | :index) on the resource — no controller needed. (There is no resource-level after_destroy_path; use the controller hook for that.)
- Need a computed path, a different flash, a custom render, or different persistence? Override the controller hook (steps below).
- Behavior that should apply to every resource/request, not one? Extend
Avo::ApplicationController (see "Extending Avo::ApplicationController").
2. Make sure the controller exists
Every resource must have a paired controller at app/controllers/avo/<name>s_controller.rb, or opening it raises ActionDispatch::MissingController. The resource generator creates it for you; if it's missing (deleted, or the resource was hand-written), generate one:
bin/rails generate avo:controller course
bin/rails generate avo:controller galaxy/planet
The last name segment is pluralized (course → Avo::CoursesController). To make generated controllers inherit from a shared base (e.g. one that adds authentication), pass --parent-controller or set it once in the initializer:
bin/rails g avo:controller city --parent-controller Avo::BaseResourcesController
Avo.configure do |config|
config.resource_parent_controller = "Avo::BaseResourcesController"
end
3. Override the hook(s) you need
Add the method(s) to the generated controller. Each hook has a documented default; calling super runs it. Guard your override with super whenever the default handles a flow you want to keep (this is the #1 source of breakage — see Gotchas).
Hooks
Every write action (create, update, destroy) exposes the same three flavors, plus two persistence methods shared by create/update and destroy. All are overridden in the generated resource controller.
Redirect path — where the user lands
after_create_path, after_update_path, after_destroy_path return the path used after a successful action.
class Avo::CoursesController < Avo::ResourcesController
def after_create_path
"/avo/resources/users"
end
end
Defaults you may be dropping:
after_create_path — when the record was created through an association, redirects back to the parent record's page; otherwise honors the resource's self.after_create_path, falling back to the new record's (or when config.resource_default_view is :edit). Return super when params[:via_relation_class] and params[:via_record_id] are present to keep the parent redirect.
after_update_path — returns params[:return_to], then params[:referrer], before honoring self.after_update_path and the / fallback. Return super when params[:return_to] is present so "send the user back where they came from" keeps working.
after_destroy_path — returns params[:referrer] when present, otherwise the resource's .
def after_update_path
return super if params[:return_to].present?
"/avo/resources/courses"
end
Flash message — what the user reads
Override *_success_message / *_fail_message (one per outcome) to change the flash text:
class Avo::CoursesController < Avo::ResourcesController
def create_success_message
"Course saved. Off you go! 🚀"
end
def destroy_fail_message
"This course could not be removed."
end
end
The six: create_success_message, create_fail_message, update_success_message, update_fail_message, destroy_success_message, destroy_fail_message. Defaults are i18n-backed (e.g. "#{@resource.name} #{t("avo.was_successfully_created")}."); destroy_fail_message defaults to the record's joined validation errors, or t("avo.failed") when there are none.
Custom response — full control over the render
When path + message aren't enough (different format, extra headers, a completely different render), override the *_action methods. These are Turbo-stream aware and their defaults drive real UI flows, so fall back to super for the cases you aren't customizing:
class Avo::CoursesController < Avo::ResourcesController
def create_success_action
return super if params[:via_belongs_to_resource_class].present?
respond_to do |format|
format.html { redirect_to "/dashboard", flash: {success: create_success_message} }
end
end
end
Defaults that carry Turbo behavior:
create_success_action — when a record is created through a belongs_to modal (params[:via_belongs_to_resource_class] present), renders Turbo Streams that close the modal and select the new record in the field; otherwise redirects to after_create_path with the success flash. Keep the super guard or the "Create new record" flow inside belongs_to fields breaks.
destroy_success_action — when the delete happens inside a Turbo Frame (params[:turbo_frame], e.g. an association list) it reloads that frame via Turbo Streams; otherwise flashes and redirects to after_destroy_path. Keep the super guard or deleting from association lists breaks.
create_fail_action / update_fail_action — flash the fail message and re-render :new / :edit with :unprocessable_content status (:unprocessable_entity on Rails < 7.1), plus a turbo_stream format.
destroy_fail_action — flashes and renders a turbo_stream alert without leaving the page.
Persistence — how the record is saved or destroyed
Avo saves with @record.save! and removes with @record.destroy!. Override save_record_action / destroy_record_action for soft deletes, archiving, a service object, or extra bookkeeping:
class Avo::CoursesController < Avo::ResourcesController
def destroy_record_action
@record.archive!
end
def save_record_action
CourseSaver.new(@record).call
end
end
Errors raised inside these methods are caught, logged, and added to the record's errors (errors.add(:base, ...)) — which automatically triggers the matching *_fail_action and *_fail_message. So you don't rescue in here yourself; raising is how you signal failure, and the record's validation errors surface in the fail flash.
Extending Avo::ApplicationController
For behavior that isn't tied to one resource — setting a Current attribute, selecting a tenant, a before_action on every admin request — you want Avo::ApplicationController, not a resource controller.
Do NOT copy Avo's application_controller.rb into your app. Avo's ApplicationController (and the BaseApplicationController it inherits) is a private API: methods, before/after actions, and helpers change between versions without a changelog or upgrade-guide entry, and a copied file silently drifts and breaks on the next upgrade. Use a concern merged in at boot instead.
Add behavior — include the concern, use an included block:
module Multitenancy
extend ActiveSupport::Concern
included do
before_action :set_tenant
prepend_before_action :set_tenant
end
def set_tenant
end
end
Rails.configuration.to_prepare do
Avo::ApplicationController.include Multitenancy
end
Override an existing method (e.g. fill_record) — prepend the concern and use a prepended block so it wins in the ancestor chain, calling super to keep Avo's behavior:
module ApplicationControllerOverrides
extend ActiveSupport::Concern
prepended do
before_action :some_hook
end
def fill_record
super
end
end
Rails.configuration.to_prepare do
Avo::ApplicationController.prepend ApplicationControllerOverrides
end
Rules of thumb: include + included to add; prepend + prepended to override an existing method; prepend_before_action to run before Avo's own filters (set a tenant/account early); always wrap it in Rails.configuration.to_prepare so it re-applies on every code reload.
Gotchas
- Guard overrides with
super — the defaults do more than redirect. after_create_path handles the via-association parent redirect (params[:via_relation_class]/via_record_id), after_update_path honors return_to/referrer, create_success_action drives the belongs_to-modal Turbo flow (via_belongs_to_resource_class), and destroy_success_action reloads the Turbo Frame for association-list deletes (params[:turbo_frame]). Drop the super branch and you break that flow. Pattern: return super if params[:x].present?, then your custom behavior.
- Never copy
Avo::ApplicationController wholesale. It's a private API that changes without changelog and breaks on upgrade. Use a concern + Rails.configuration.to_prepare (include/included to add, prepend/prepended to override, prepend_before_action to run first).
- Raising is how you fail in persistence hooks. Errors from
save_record_action / destroy_record_action are caught and added to @record.errors[:base] → the fail action + message fire automatically. Don't swallow the exception; let it raise (or errors.add yourself) to signal failure. A foreign-key constraint on delete surfaces this way instead of 500-ing.
- Prefer the resource option when one exists. For a plain "land on index/edit after save",
self.after_create_path / self.after_update_path on the resource beats a controller override. There's no resource-level after_destroy_path — that one's controller-only.
- Missing controller →
ActionDispatch::MissingController. Every resource needs its controller; generate it with bin/rails g avo:controller <name>. Use --parent-controller (or config.resource_parent_controller) to inherit from a shared base.
- Verify before writing. Method names and defaults drift between versions — check the docs URLs above or the app's installed Avo source (
Avo::BaseController, ) rather than trusting memory.
Related skills: setting the tenant per request → avo-multitenancy; Avo::Current and other engine internals → avo-engine-internals; authenticating admin requests / config.authenticate_with → avo-authentication; resource-level self.after_create_path and friends → avo-resources.
Report
When done, tell the user:
- Which controller file(s) you created or edited (full paths) and any generator command(s) run.
- Which hook(s) you overrode and what each now does — and, for any hook whose default carried a Turbo/association/
return_to flow, confirm you kept the super guard (or explicitly note you dropped it and why).
- For persistence overrides (
save_record_action/destroy_record_action): what now persists/removes the record, and that failures still surface through the normal fail action/message.
- For
Avo::ApplicationController work: the concern file, whether you used include/prepend, and that it's wired in Rails.configuration.to_prepare.
- Anything still needed: run pending migrations, generate a missing controller, add the
archived_at/soft-delete column, or reach for a related skill (multitenancy, authentication).