Skip to content

Upgrade guide ​

We'll update this page when we release new Avo 4 versions.

If you're looking for the Avo 3 to Avo 4 upgrade guide, please visit the dedicated page.

How to read this page

Entries are in release order, newest first. Find the release you're on, then work up the page to the one you're installing, doing each entry for the gems you use.

To see which versions you run, open your app's status page at /avo/status (or /<your mount path>/status). It lists every Avo gem you have installed and marks the ones with a newer release.

To open this page at your release, add ?since= with a date or a gem version: upgrade.html?since=2026-10-14 or upgrade.html?since=avo@4.3.2.

Show entries for (0 shown)

avo-audit_logging 4.4.2 Unreleased ​

Changes made outside Avo are logged by default

Breaking Change ​

outside_avo now defaults to true. A resource that opts in with self.audit_logging logs every create, update and delete on its model wherever it happens, such as in a job, an importer, the console or your app's own pages. It needs PaperTrail on the model, so a model without has_paper_trail logs what it did before. In the panel, a create's entry now links its version, so its page shows what the record was created with. See Log changes made outside Avo.

Action Required ​

Check each resource that opts in and whose model has PaperTrail.

If your code calls Avo::AuditLogging.record after its own saves, each of those changes is now logged twice. Wrap the save and the call in Avo::AuditLogging::Recorder.claiming, so the call's entry takes the save's version, or set outside_avo: false on the resource as shown below.

ruby
# app/services/price_updater.rb
Avo::AuditLogging::Recorder.claiming do
  product.update!(price: price)
  Avo::AuditLogging.record(action: :update, record: product, author: user, payload: {price: price})
end

An activity block now also runs for changes made outside Avo, where current_user is nil. A block that calls a method on it, such as current_user.audit_avo_activity?, raises there. The change is still saved, but nothing is logged and the error goes to your log. Make the block handle nil. With &. it skips those changes, and current_user.nil? || current_user.audit_avo_activity? logs them.

ruby
# app/avo/resources/product.rb
self.audit_logging = {
  activity: -> { current_user.audit_avo_activity? } 
  activity: -> { current_user&.audit_avo_activity? } 
}

Expect more entries in the audit log, one for each change your jobs and imports make. Your test suite logs the records its setup creates too, so a spec that counts entries may count more. Wrap work you don't want logged, such as an import or a factory call, in Avo::AuditLogging.disabled { ... }.

Maintaining Previous Behavior ​

Set outside_avo: false on each resource that should log only what happens in Avo.

ruby
# app/avo/resources/product.rb
class Avo::Resources::Product < Avo::BaseResource
  self.audit_logging = {
    activity: true,
    outside_avo: false
  }
end

avo-ai 4.4.1 Unreleased ​

Opening the sidebar on smaller screens temporarily hides the floating chat bar so it cannot cover the profile menu. Closing the sidebar restores the bar, including the current conversation and draft. Desktop navigation is unchanged. See Open the chat.

Action required: None.

avo-audit_logging 4.3.3 Unreleased ​

Audit logging is configured inside Avo.configure

Deprecation ​

The audit logging options moved under config.audit_logging in Avo.configure. The standalone Avo::AuditLogging.configure block still applies its options, but logs a deprecation warning on boot. It will be removed in Avo 5.

Action Required ​

Only if config/initializers/avo.rb (or another initializer) calls Avo::AuditLogging.configure. Move each option into Avo.configure under config.audit_logging and delete the old block. The options and their values are unchanged: enabled, author_model, author_models and payload_modifier.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  # ...
  config.audit_logging.enabled = true
  config.audit_logging.author_models = ["User", "Account"] 
end

Avo::AuditLogging.configure do |config| 
  config.enabled = true
  config.author_models = ["User", "Account"] 
end

Code that reads Avo::AuditLogging.configuration keeps working and returns the same object as Avo.configuration.audit_logging.

avo-notifications 4.3.2 Unreleased ​

Notifications are configured inside Avo.configure

Deprecation ​

The notifications options moved under config.notifications in Avo.configure. The standalone Avo::Notifications.configure block still applies its options, but logs a deprecation warning on boot. It will be removed in Avo 5. The installer no longer writes config/initializers/avo_notifications.rb; it adds the options to config/initializers/avo.rb instead.

Action Required ​

Only if config/initializers/avo_notifications.rb (or another initializer) calls Avo::Notifications.configure. Move each option into Avo.configure under config.notifications and delete the old block, or the whole file if nothing else is in it. The options and their values are unchanged: ttl, realtime, dropdown_limit, user_class and user_display_name_method.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  # ...
  config.notifications.ttl = 7.days
  config.notifications.realtime = false
end

# config/initializers/avo_notifications.rb
Avo::Notifications.configure do |config| 
  config.ttl = 7.days
  config.realtime = false
end

Code that reads Avo::Notifications.configuration keeps working and returns the same object as Avo.configuration.notifications.

avo-meta 0.1.3 Unreleased ​

Meta is configured inside Avo.configure

Deprecation ​

The fields_position option moved under config.meta in Avo.configure. The standalone Avo::Meta.configure block still applies it, but logs a deprecation warning on boot. It will be removed in Avo 5.

Action Required ​

Only if an initializer calls Avo::Meta.configure. Move the option into Avo.configure and delete the old block.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  # ...
  config.meta.fields_position = :top
end

Avo::Meta.configure do |config| 
  config.fields_position = :top
end

avo-dynamic_filters 4.3.3 Unreleased ​

This release adds three things to the filter bar. None of them change your app until you turn them on.

  • Pinned filters. filterable: {pinned: true} keeps a filter's pill in the bar, so people can use it without opening "Add filter" first.
  • The new filter dropdown. config.dynamic_filters.simple_filters = true gives each filter type its own dropdown: a condition menu, then a checklist, a row of buttons, date presets or a box. It filters as you click, with no Apply button. It is off by default and becomes the default in a future release.
  • Inline filters. With the new dropdown on, filterable: {pinned: true, inline: true}, or config.dynamic_filters.inline_filters = true for every pinned filter, puts a filter's control straight in the bar, like Yes / No buttons for a boolean. Date filters also take presets, and nullable: true adds an Empty choice.

What changed ​

The new dropdown and inline filters ship off, so an app keeps the older dropdown until it opts in. The new dropdown becomes the default in a future release.

Action Required ​

Turn both on:

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  # ...
  config.dynamic_filters.simple_filters = true
  config.dynamic_filters.inline_filters = true
end

inline_filters only changes pinned filters. If the app pins none, pin the two or three filters its users reach for most, with filterable: {pinned: true} on those fields.

Then run your system tests. A test that picks a condition from a select, types a value and clicks Apply drives the older dropdown: update it to pick from the condition menu and the value control, as The filter dropdown describes. If you can't update them yet, keep it on everywhere but the test environment with config.dynamic_filters.simple_filters = !Rails.env.test?.

Filter pills show the name, condition and value as separate segments

What changed ​

A pill reads Name | is | Austin rather than Name is "Austin", without the quotes. "Add filter" and "Reset filters" sit after the pills. This reaches every app, with or without the options above.

Action Required ​

None, unless a system test or custom CSS relies on the old pill text or on the position of those two buttons.

Several negative values on one filter exclude all of them

What changed ​

Several Is not or Does not contain values on one attribute are now combined with AND, as How conditions combine describes. They used to be combined with OR, which matched nearly every record. This reaches every app.

Action Required ​

None, unless you link to a filtered URL with several negative values. It now returns fewer records.

Dynamic filters are configured inside Avo.configure

Deprecation ​

The dynamic filters options moved under config.dynamic_filters in Avo.configure, the same way config.ai and config.mcp_server work. The standalone Avo::DynamicFilters.configure block still applies its options, but logs a deprecation warning on boot. It will be removed in Avo 5.

Action Required ​

Only if config/initializers/avo.rb calls Avo::DynamicFilters.configure. Move each option into Avo.configure under config.dynamic_filters, and delete the old block with its if defined?(Avo::DynamicFilters) guard. The options and their values are unchanged: button_label, always_expanded and param_key.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  # ...
  config.dynamic_filters.button_label = -> { I18n.t("my.filters") } 
  config.dynamic_filters.always_expanded = false
end

if defined?(Avo::DynamicFilters) 
  Avo::DynamicFilters.configure do |config| 
    config.button_label = -> { I18n.t("my.filters") } 
    config.always_expanded = false
  end
end

Code that reads Avo::DynamicFilters.configuration keeps working and returns the same object as Avo.configuration.dynamic_filters.

avo-mcp_server 4.3.0.beta.3 Unreleased ​

ConnectionPolicy#use? turns the server on or off per admin

Breaking Change ​

The server now asks use? on Avo::McpServer::ConnectionPolicy before it serves an admin: on every call, on code exchange and refresh, on the consent screen and on the connect page. A ConnectionPolicy without use? turns the server off for every admin. See Turn MCP on or off per admin.

Action Required ​

If your app has an Avo::McpServer::ConnectionPolicy, add use? to it:

ruby
# app/policies/avo/mcp_server/connection_policy.rb
class Avo::McpServer::ConnectionPolicy < ApplicationPolicy
  def use? = true 
end

If it has none, Avo's defaults decide, as for any missing policy. Under explicit_authorization = true, which is Avo's default, the server is off for every admin until you add the policy above. Under explicit_authorization = false it stays on. With raise_error_on_missing_policy = true, the missing policy raises on the server's endpoints instead. The policy also decides who sees the connections resource, so see Decide who sees and revokes what for its other methods.

With config.authorization_client = nil no policy runs: the server stays on, use? is never asked, and nobody is blocked.

With avo-permissions as your authorization client, no ConnectionPolicy is asked and explicit_authorization doesn't apply: a role needs a use? permission on Avo::Resources::McpConnection, or the server is off for it. The permissions screen doesn't offer it yet, so add the row from a console for each role that should keep MCP:

ruby
Avo::Permissions::Permission.create!(role: role, resource_class: "Avo::Resources::McpConnection", method: "use?")

A tool of your own declares the resources it touches

Breaking Change ​

A connection narrowed to named resources now refuses a call to a tool of your own that declares no resources and sends no resource argument its schema declares, because the server can't tell what it reads. Before, such a call ran on those connections. On every connection, a tool of your own is now also refused when the connecting admin's index? refuses a resource it declares or names, and when its resource argument matches no resource. See Add tools of your own.

Action Required ​

Only if you register tools through config.mcp_server.extra_tools. Declare the resources each one reads or changes. A tool that reaches no resource at all, say one that only calls an outside API, has nothing to declare, so it can't run on a narrowed connection. With avo-permissions as your authorization client, a call that names a resource, through resources or a resource argument, is refused on every connection whatever rows the role holds, because the server's policy checks don't pass that client the resource its rows match on. The shipped tools already behave this way, and list_resources and search_records with no resource answer empty there. A tool with neither still runs on a connection that isn't narrowed.

ruby
# app/tools/ticket_summary_tool.rb
class TicketSummaryTool < Avo::McpServer::Tool
  capability "avo:read"
  resources "Ticket", "Project"
end

# Or, on a RubyLLM::Tool shared with Avo AI:
def self.resources = %w[Ticket Project]

avo-advanced_search 4.3.3 Unreleased ​

Breaking Change ​

On the dedicated results page, Mod+k now focuses the navbar search, just as it does elsewhere. It no longer focuses the page input on the first press and the navbar on a double-tap. Use Mod+Shift+K to focus the page input instead. See Global search.

Action Required ​

Action required: None. To customize either shortcut, use advanced_search.open for the navbar and advanced_search.focus_results for the page input in config.hotkeys[:bindings].

avo-api 4.4.0 Unreleased ​

Breaking Change ​

A belongs_to or has_one field now reads back with the related record's to_param, the id in its panel URL, instead of its primary key. It's always a string: { "id": "5", "label": "John Doe" }, not { "id": 5, "label": "John Doe" }. Where a model hides its primary key (friendly_id, a prefixed id, a custom to_param), the response carries the slug or prefixed id instead of the key. The OpenAPI document types both ids as strings. See Field serialization.

A belongs_to write takes that value back, also on a resource found by slug alone. A record's own id field doesn't change.

Action Required ​

Update API clients that read these ids as numbers or compare them with numbers. Send back the value a read returns.

avo-ai 4.3.0.beta.10 Nov 9, 2026 ​

ChatPolicy#use? turns the chat on or off per user

Breaking Change ​

The chat now asks use? on Avo::Ai::ChatPolicy before it serves a user: on the chat bar, on every chat page and request, and in a reply or title already queued. The policy avo-ai ships answers it for every signed-in user, but a ChatPolicy of your own without use? turns the chat off for everyone. See Turn the chat on or off per user.

Action Required ​

If your app has its own Avo::Ai::ChatPolicy, add use? to it:

ruby
# app/policies/avo/ai/chat_policy.rb
class Avo::Ai::ChatPolicy < ApplicationPolicy
  def use? = user.present?
end

With avo-permissions as your authorization client, a role needs a use? permission on Avo::Resources::AvoAi::Chat to keep the chat, and the permissions screen doesn't offer it yet. Add the row from a console for each role that should keep the chat:

ruby
Avo::Permissions::Permission.create!(role: role, resource_class: "Avo::Resources::AvoAi::Chat", method: "use?")

With config.authorization_client = nil no policy runs: the chat stays on, use? is never asked, and nobody is blocked.

avo-dashboards 4.3.4 Nov 9, 2026 ​

Cards load when they scroll into view

Breaking Change ​

Cards used to load as soon as the page rendered, all at once. Now a card waits until it scrolls into view, so a card further down a long dashboard or resource page runs its query only when the viewer reaches it. See Load cards only when they're needed.

Action Required ​

Action required: None. If your system tests assert on a card below the fold, scroll to it first.

Breaking Change ​

Dashboards can now run actions. Avo calls a dashboard's actions method on every render and shows what it declares in an Actions dropdown. A dashboard that already defines actions for its own purposes now has that method called, and action or divider inside it mean something new.

To run those actions, a dashboard now also answers to action, get_actions, visible_actions, find_action, view, view_type, records_path, record, user, params, model_key, route_key, authorization, hydrate, resource_name_for, translation_key and get_stimulus_controllers. A method of yours with one of these names replaces Avo's, and an action started from that dashboard uses yours.

Action Required ​

Action required: Only if a class in app/avo/dashboards defines one of these methods. Otherwise, none.

Steps to Update ​

Rename your method.

ruby
# app/avo/dashboards/operations.rb
def actions
def queue_actions
  # ...
end

avo 4.3.11 Nov 4, 2026 ​

Session persistence also keeps the sort, the resource table page, and the selected rows

Breaking Change ​

With config.persistence = { driver: :session }, the session used to keep only association pagination and static filter selections. It now also keeps the page and per_page of resource tables (the per_page cookie is no longer read), the sort picked from a table header, and the rows checked on a table, which stay in the browser tab. Dynamic filter selections are kept too, with avo-dynamic_filters 4.3.2 or newer. A plain visit, such as the sidebar link, lands on the remembered page, sorted and filtered as the user left it, with the rows still checked. Nothing changes without the driver. See Persist UI state.

Action Required ​

None, if you want the new state to stick. Check your system specs: one that sorts, pages, or selects rows with the driver on and then expects a plain visit to start fresh now finds that state still there.

Maintaining Previous Behavior ​

Turn the new kinds off next to the driver. pagination covers resource and association tables together, so association pagination alone can't be kept.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.persistence = {
    driver: :session,
    sorting: false,
    selection: false
  }
end

avo 4.3.9 Oct 28, 2026 ​

act_on? authorizes running an action, not only the Actions button

Breaking Change ​

Running an action now asks the resource policy's act_on? of the record on the page and of each selected record (the model class when there are none, and for Select all). If any is denied, the whole run is refused. Before, act_on? only hid the Actions button, so a hand-built request could run an action on records the policy denies.

An action run from an association panel is still shown or hidden by the parent's act_on_{association}?, and now also needs act_on? on each selected child.

Action Required ​

None, unless a resource's actions run only from association panels and its policy has no act_on?. Under explicit_authorization (the default), those runs are now refused. Define it on the child's policy:

ruby
# app/policies/comment_policy.rb
class CommentPolicy < ApplicationPolicy
  def act_on?
    user.admin?
  end
end

A refusal with no page to go back to renders a 403 page

Breaking Change ​

A refused request with no page to go back to (an address typed in, a bookmark, a reload) now gets a 403 page in the Avo layout with the "not authorized" message, instead of a redirect to Avo's home page. JSON and Turbo Stream requests get a bare 403. A request with a page to go back to is still redirected there with the flash.

Before, a refused home page redirected to itself until the browser gave up, and a refusal inside an add-on's own pages raised a 500.

Action Required ​

None, unless a test or client expects the redirect to the home page.

avo-authorization 4.3.3 Oct 28, 2026 ​

An app with an authorization client and no avo-authorization license refuses every resource

Breaking Change ​

When your initializer sets authorization_client and your license doesn't include the avo-authorization add-on, Avo now refuses every resource: lists come back empty, show and edit answer 404, and a banner explains why. Before, every policy was skipped and every admin reached every record. See When the license lapses.

Action Required ​

Check that every environment where your initializer sets a client runs with your license key, test and CI included. Apps with no authorization_client line, or with it set to nil, are unaffected.

avo-ai 4.3.0.beta.8 Oct 28, 2026 ​

The assistant reads a record's fields and files only when show? allows it

Breaking Change ​

The assistant now asks show? before it reads a record's field values, its files, or its chip fields. A row the user may list but not open comes back with only its id and reference. Before, index? and the policy scope were enough.

Action Required ​

None, unless users should read rows through the chat that their show? refuses. Widen show? for them.

The **Agent** button and the new-chat page render _pit_core_icon

Breaking Change ​

The Agent button and the full-page new-chat page now render avo/ai/_pit_core_icon instead of avo/ai/_avocado_icon. An app that replaced the assistant's icon with its own _avocado_icon partial still changes the empty states and the Open in assistant bar button, but these two go back to Avo's own mark. See Replace the assistant's icon.

Action Required ​

None, unless you override _avocado_icon. If you do, override _pit_core_icon as well, passing classes through:

erb
<%# app/views/avo/ai/_pit_core_icon.html.erb %>
<%= svg "tabler/outline/robot", class: local_assigns[:classes] %>

avo-api 4.3.1 Oct 28, 2026 ​

Breaking Change ​

An association field is now left out of a response when the record's policy refuses view_<field>?, or, under explicit_authorization (the default), doesn't define it. Counts and has_one ids go through the related resource's Scope, so a has_one outside it is null. A has_and_belongs_to_many field is now { "count": 12 }, like a has_many, instead of the related rows. See Field serialization.

Action Required ​

Define view_<field>? for every association field your API clients read, and update clients that read a has_and_belongs_to_many field as a list.

ruby
# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
  def view_posts? = true 
end

avo-audit_logging 4.3.1 Oct 28, 2026 ​

Undo needs update? on the record

Breaking Change ​

The Undo change action now asks the record's policy: update?, with the record found through its policy scope, or create? to bring back a destroyed record. Users the policy refuses no longer see the action. The activity's payload also leaves out fields the user's field policy withholds, as the changeset diff already did.

Action Required ​

None, unless an STI model records its versions under a base class that has no Avo resource. Undo is hidden for those until you map the base class with model_resource_mapping.

avo-collaboration 4.3.2 Oct 28, 2026 ​

Timeline requests need access to the record and its timeline

Breaking Change ​

Every collaboration request now finds the record through the resource's policy scope and asks show? and collaboration_view_timeline?. Posting also asks collaboration_create_entry?, and deleting asks collaboration_destroy_entry?. Before, these methods only hid buttons. A refused request gets a 403, and a record outside the scope a 404. See Authorization.

Action Required ​

None. The timeline's buttons already followed these methods.

avo-dashboards 4.3.2 Oct 28, 2026 ​

A card's URL refuses what its page refuses

Breaking Change ​

A card's own URL now refuses a card of a dashboard whose authorize fails, a card hidden by visible, and a resource card whose policy refuses index? (or show? on the record). Before, it served any card by its position. See Cards visibility.

Action Required ​

None. Cards render on their pages as before.

avo-dynamic_filters 4.3.1 Oct 28, 2026 ​

A filter applies only the conditions it offers

Breaking Change ​

A condition in the URL that the filter doesn't offer is now ignored, and no pill shows for it. Before, Avo passed any condition to Ransack. That includes is_present and is_blank on number and select filters, which only text filters offer. See conditions.

Action Required ​

None, unless a link or bookmark uses a Ransack predicate the filter doesn't list. Add it to the filter's conditions, together with the default conditions you want to keep.

avo-http_resource 4.3.1 Oct 28, 2026 ​

The debug console runs a probe only when the policy allows that action

Breaking Change ​

Each debug console probe now asks the policy method the resource's own page would: index? for index and count, then show?, create?, update?, and destroy? for delete. A refused probe is reported as not authorized and never sent. Before, index? was enough to run every probe, writes to the remote API included.

Action Required ​

None, unless the console's users should run write probes. Allow create?, update?, or destroy? for them. These are asked of the model class, so a method that reads the record refuses.

avo-kanban 4.3.2 Oct 28, 2026 ​

Board routes follow the board policy, and cards their record's policy

Breaking Change ​

Every board route now asks the Avo::Kanban::BoardPolicy method behind its button and finds the board through the policy's Scope. Removing a card asks manage_column?, and resetting a column is now a POST. Moving or adding a card updates its record, so it needs the record inside its resource's policy scope, update? on it, and a field on that resource that writes the board's property. Cards whose record the scope hides are no longer shown. See Authorization.

Action Required ​

  • Allow update? on the records your users move on a board.
  • If a board's property is a virtual attribute with no field, add one to the resource. See Items without that property.

avo-mcp_server 4.3.0.beta.2 Oct 28, 2026 ​

Breaking Change ​

The tools now leave out an association the record's policy refuses with view_<association>?, or, under explicit_authorization (the default), doesn't define. A has_one is read through the associated resource's policy scope and is null outside it, and has_many ids go through that resource's scope. See Arguments.

Action Required ​

Define view_<association>? for every association MCP clients should read.

records are named by to_param

Record ids are strings, resolved through the resource's own finder

What changed ​

The MCP tools name every record by its to_param, the id in the panel's URL, and find it through the resource's find_record_method, within the admin's policy scope. They used to use the primary key.

  • Ids are strings. list_records, search_records, show_record and the write tools return a record's id as "12", not 12, and so do a belongs_to's id, a has_many's ids, and run_action's recordIds. The id and record_ids arguments take strings; an integer is refused with value at /id is not a string.
  • Hidden keys stay hidden. Where a model hides its primary key (friendly_id, hashids, a prefixed id, a custom to_param), tools return the slug or hashid instead of the primary key, and take whatever the resource's finder takes. A record with no title attribute is titled Post #<to_param>.
  • A belongs_to takes back the id show_record gives, also on a resource found by slug alone.

Action Required ​

  • A client, prompt or skill that sends or compares ids as numbers: treat ids as strings and send back what the tools return.
  • A tool of your own: declare its id arguments type: "string", pass them to ToolSupport.find_authorized_record! as received, and name records by record.to_param in what it returns. See Add tools of your own.

avo-menu 4.3.1 Oct 28, 2026 ​

Breaking Change ​

A menu action item now shows only when the resource's policy allows act_on?, as the resource's own Actions dropdown does. Under explicit_authorization (the default), a policy without act_on? hides it.

Action Required ​

None, unless the resource's policy lacks act_on?. Define it.

all_boards lists only the boards the board policy's Scope returns

Breaking Change ​

all_boards now lists only the boards Avo::Kanban::BoardPolicy::Scope returns. Before, it listed every board.

Action Required ​

None.

avo-nested 4.3.2 Oct 28, 2026 ​

A nested form writes only the children the user may update or create

Breaking Change ​

Saving a parent with a nested form now asks the child's policies. An existing child is written only when it's inside the child resource's policy scope and update? allows it; other rows are skipped. A new child needs the child's create? and the parent's create_<association>?, and the "Add" button is hidden when create_<association>? refuses. Under explicit_authorization (the default), a parent policy without create_<association>? refuses new rows. See Nested in Forms.

Action Required ​

Define create_<association>? on the parent's policy for every nested association users add records to:

ruby
# app/policies/book_policy.rb
class BookPolicy < ApplicationPolicy
  def create_authors? = user.admin?
end

avo-notifications 4.3.1 Oct 28, 2026 ​

The notification resource finds records through the current user

Breaking Change ​

The generated notification resource scoped the list to the current user, but not the record lookup. A signed-in user could open, delete or run actions on another user's notification by id. New installs generate a resource that scopes both. See Notification resource.

Steps to Update ​

If you generated app/avo/resources/avo_notification.rb with avo-notifications 4.3.0 or earlier, add the lookup below index_query:

ruby
# app/avo/resources/avo_notification.rb
class Avo::Resources::AvoNotification < Avo::BaseResource
  self.index_query = -> {
    query.for_user(Avo::Current.user).newest_first
  }

  self.find_record_method = -> { 
    query.for_user(Avo::Current.user).find(id) 
  } 
end

avo-record_reordering 4.3.1 Oct 28, 2026 ​

Moving a record needs reorder? on its policy

Breaking Change ​

Moving a record now asks reorder? on its own policy, from any panel. Association panels still show the controls by reorder_<association>?, but the move also needs the record's reorder?. After a move, Avo redirects back only to an address inside the app.

Action Required ​

Define reorder? on the policy of every resource users reorder from an association panel:

ruby
# app/policies/course_link_policy.rb
class CourseLinkPolicy < ApplicationPolicy
  def reorder? = user.admin?
end

avo-scopes 4.3.1 Oct 28, 2026 ​

A scope count asks the same policy as the index it counts

Breaking Change ​

A scope's count now asks index?, or on an association panel the parent's view_<association>?. A refused count renders an empty pill. Before, a user could read the counts of a resource whose index their policy denies.

Action Required ​

None. A user who can see the index sees the same counts.

avo 4.3.8 Oct 28, 2026 ​

This release closes places where a signed-in user could reach a record past the policy the resource's own pages ask (GHSA-5qcq-2vq9-62hj). Each entry below is a policy that now runs where none ran before, so read them against the policies you have.

update? is asked of the record as stored and again with the submitted values

Breaking Change ​

Saving an edit now asks update? twice: once with the record as it is stored, then again once the submitted values are assigned. Both have to pass. Before, only the filled record was authorized, so a value the user submitted could make a record pass a policy it fails as stored, such as a new owner under an owner-only update?. A react_on re-render of an open edit form asks edit? the same way.

Action Required ​

None for a policy that answers from the user and the record's stored values. A policy that passes only once the user submits a value is now refused by the first check, and a policy that does work of its own does it twice.

ruby
# app/policies/project_policy.rb
class ProjectPolicy < ApplicationPolicy
  # Asked before and after the form's values land on the record, so submitting
  # your own id as the new owner no longer makes this pass.
  def update?
    record.user_id == user.id
  end
end

Listing a parent's association by URL asks view_{association}?

Breaking Change ​

Every request that lists a parent's association now asks the parent policy's view_{association}?, as the panel itself does: the panel's rows, its search, the chart of a summarizable column in it, and scope counts under it. The request also has to name the resource the association field lists with, and answers 404 otherwise. Before, these requests listed a parent's association under any resource, whatever the parent's policy said.

Action Required ​

None in most apps: a panel is only shown when view_{association}? allows it, so the same method now answers the requests behind it. Two cases to check:

  • A link or bookmark you build by hand to an association list has to name the field's resource, which is its use_resource when the field sets one.
  • Under explicit_authorization (the default), a parent policy with no view_{association}? refuses these requests, as it already hid the panel.
ruby
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def view_comments? = true 
end

A summarizable column's chart asks index?

Breaking Change ​

The distribution chart behind a summarizable column ran no policy check. It now asks index?, or, for a column on an association panel, the parent's view_{association}?. Its rows were already narrowed by the policy Scope; what changes is that a user whose index? refuses the list no longer gets its distribution either.

It asks index? and not a chart method of its own: distribution_chart? and distribution_chart_full? are not policy methods Avo asks for, explicit_authorization included.

Action Required ​

None. A user who can open the index sees the same chart.

Breaking Change ​

The request that renders a has_one panel now finds its record through the related resource's Scope, and answers 404 for a record outside it. Detaching a has_one clears the association only when the URL names the record currently attached, and answers 404 otherwise, so a detach request can no longer clear the association by naming some other record.

Action Required ​

None, unless a related resource's Scope hides records your users open from a has_one panel. Widen that Scope for them.

avo 4.3.6 Oct 27, 2026 ​

Trix uploads on the New form are authorized with create?

What changed ​

A Trix field on an Action Text attribute, or on a plain column with attachments_disabled: false, now accepts uploads on the New form. Those uploads are authorized with the resource policy's create?. Uploads on a saved record are still authorized with update?, and attachment_key uploads with upload_{FIELD_ID}?.

Action Text attachments, images and other files, now keep their sgid and render on the Show view through your app's active_storage/blobs/_blob.html.erb partial. Before, non-image files rendered as missing (#2715).

Action Required ​

None, unless your create? policy is looser than update? and you want to keep users who can create a record from uploading files before it's saved. Set attachments_disabled: true on that field.

avo-ai 4.3.0.beta.6 · avo-mcp_server 4.3.0.beta.2 · avo-menu 4.3.1 Oct 27, 2026 ​

an action's visible block applies everywhere

What changed ​

The assistant, the MCP tools and a menu action item now check an action's self.visible block as well as its self.authorize, the way the panel's Actions dropdown does.

  • The assistant and list_actions leave out an action that is hidden on both the index and a record's page, and refuse to run one that is hidden on the page the run starts from: a record's page for one record, the index otherwise.
  • A menu action item is left out when its action is hidden on the resource's index.

Action Required ​

None, unless an action hides itself with self.visible but is still meant to run from the assistant, an MCP client or the menu. Move that rule into self.authorize, or widen self.visible.

avo 4.3.5 Oct 24, 2026 ​

bulk actions find the selected records through find_records

query is an Array, in the order the records were selected

What changed ​

A bulk action now looks up the records the user selected through the resource's new find_records, the same lookup the assistant and the MCP tools use.

  • query is an Array, in the order the records were selected. It used to be an ActiveRecord::Relation on a resource whose find_record_method returns query.where(...) for a list of ids, FriendlyId resources included. Select all still passes a relation.
  • A find_record_method that ignores query stays inside the user's policy scope. The records it returns that the scope doesn't hold drop out of query.
  • A find_record_method that takes one id at a time works for bulk actions. Avo asks it once per id when it fails on the whole list.

Action Required ​

None, unless a bulk action calls a relation method such as update_all, where or find_each on query. Iterate over it instead, or build a relation with Model.where(id: query.map(&:id)).

attaching a has_many, has_one or HABTM record follows attach_scope

What changed ​

The attach modal of a has_many, has_one or has_and_belongs_to_many field listed the records its attach_scope returned, but saving looked the chosen record up through the policy scope alone. A record outside a narrowing attach_scope could still be attached by sending its id, and one a widening attach_scope listed was refused. Saving now goes through the same attach_scope as the list, the way a belongs_to already did. A record outside it is refused like an id that doesn't exist.

Action Required ​

None for the modal's own list, which offers only records inside the attach_scope. A searchable picker backed by a custom search provider returns hashes, which the attach_scope can't filter, so make that search return only records inside it. Anything that posts ids to the attach endpoint directly gets the same check.

a belongs_to value that matches no record is a form error

The form comes back with the error on the field, and the REST API answers 422

What changed ​

A belongs_to value that matches no record the field may attach, because the record is gone or outside the field's attach_scope, used to answer with a 404 page, and avo-api with a 404. The form now comes back with "is invalid" on that field, and the API answers 422 with the field named in errors, the way a validation error does. Nothing is saved.

Action Required ​

None, unless an API client reads a 404 from a create or update as "the related record doesn't exist". It gets a 422 now; read its errors.

A form with an unresolved belongs_to keeps the user's other edits and saves no uploads

What changed ​

The form that comes back with an unresolved belongs_to keeps every other edit the user made, and puts "is invalid" on every belongs_to the submit couldn't resolve, not only the first one. A files field's uploads are now saved with the record rather than while the form is filled, so a form that answers 422 stores no files.

Action Required ​

None.

a field declared once per view saves through the form's declaration

Save rules come from the declaration the form shows

What changed ​

When a resource declares the same field more than once for different views, saving the form now goes through the declaration the form shows. Before, the last declaration wrote the value, whichever view it was for, so the form's attach_scope, update_using and other save rules could be skipped. See Show and hide fields on different views.

A money field that receives only one of its params now keeps the record's current value for the other, and a polymorphic belongs_to that receives only its <as>_id keeps the current type. An <as>_type sent alone keeps the current record when it is the current type. A new type needs its <as>_id, since the current id would name a different record: a linked record sent a new type alone gets "is invalid" on the field, and nothing is saved. Before, an <as>_id or a currency sent alone was dropped, an <as>_type sent alone cleared the id, and an amount sent alone cleared the currency. The panel's forms always send both; this shows in the REST API, the assistant and MCP clients.

Avo::BaseResource#fields_by_database_id is replaced by fields_by_param, which maps every form param to the field that writes it.

Action Required ​

Search your resources for a field declared more than once. Where the declarations have different save rules, the one on the form now applies. Replace any call to fields_by_database_id with fields_by_param.

avo-ai 4.3.0.beta.5 · avo-mcp_server 4.3.0.beta.2 Oct 24, 2026 ​

writes go through the resource's fields

Creates and updates run each field's save rules and take the form's names

What changed ​

The assistant and connected MCP clients used to write the values they were given straight onto the model. They now save the way the panel's edit form does, through your fields:

  • Save rules run: update_using, a belongs_to looked up through its resource's finder and attach scope, a polymorphic field's types, and a key-value field's locks.
  • Names are the form's: a field's id, or the param the form submits. A column a field writes under another name is refused by name: tag_list (write tags), a money field's price_cents, a location's stored_as columns, a for_attribute: field's attribute. On the MCP server, so is a field the edit form doesn't render, such as a badge.
  • Paired params: a money field with enable_currency takes its amount and currency, either of which sent alone keeps the record's current value for the other. A polymorphic belongs_to takes its _type and _id: an _id sent alone keeps the current type, and a _type naming another type needs its _id.

Undo in the chat still restores the stored row exactly as it was.

Action Required ​

  • A client, prompt or skill that writes raw columns: switch to the names the refusal lists, which are the ones list_resources and the edit form use.
  • A tool of your own that writes records: write through the fields — Avo::Ai::FieldScope#fill for the assistant, FieldSupport.write_record! for the MCP server. See Bring your own tool.
  • An update_using that isn't safe to run twice now also runs on chat and MCP writes, as it does on the panel's.

avo-ai requires ruby_llm ~> 2.1

Breaking Change ​

avo-ai 4.3.0.beta.4 allowed RubyLLM's 2.0 line (>= 2.0.0.rc4, < 2.1). 4.3.0.beta.5 requires ~> 2.1, the release line whose MCP client MCP connections are built on.

Action Required ​

None, unless your own Gemfile pins ruby_llm to the 2.0 line: bundle update avo-ai pulls RubyLLM 2.1 along with it, and refuses to resolve while that pin stays. Raise it, or drop it:

ruby
# Gemfile
gem "ruby_llm", "~> 2.0.0"
gem "ruby_llm", "~> 2.1"

avo 4.3.4 Oct 22, 2026 ​

Breaking Change ​

GET /resources/:resource/:id/:related_name/:related_id renders a related record's show page. A has_one panel loads its frame from it, and for every other association it is reachable by URL. It asked no policy at all: anyone who could see the parent record could open any record associated with it, whatever the related resource's show? or policy scope said. This closes GHSA-x65r-p6hf-94hv.

The route now runs the checks the rest of Avo already ran:

  • The parent's view_{association}?, as the association's index already did.
  • The related record's own show?, as the resource's show page does.
  • A has_many row is looked up inside the related resource's policy scope, so a record the scope doesn't hold is not found.

A denied policy method is refused the way any unauthorized page is; a row outside the policy scope behaves like an unknown id.

Action Required ​

None if your policies already describe what your admins should reach. Three cases change what a user sees:

  • A related resource whose show? is stricter than the parent's. URLs that opened before are now refused. Moving the rule to show_{association}? only hides the row's button; it does not grant access to the page.
  • A has_one field pointing at a record its resource's show? denies. The panel's frame is refused instead of rendering the record. Allow show? for it, or hide the field with visible.
  • A policy scope that excludes records still listed in a panel. Opening such a row now behaves like an unknown id.

avo 4.3.3 Oct 21, 2026 ​

Non-standalone actions on an association panel need a selected row

Breaking Change ​

In a has_many panel, an action that isn't standalone is disabled until you check a row, the same as on the resource's Index view. Before, the panel built its actions with the parent record, the record whose Show page holds the panel, so Avo treated every action as already having a target and let you open and run it with nothing selected (#4904). See Run actions from an association panel.

Action Required ​

An action that shows on an association panel and reads record in its name or arguments block now gets nil there, as it already did on the main Index view. Guard the block:

ruby
# app/avo/actions/release_fish.rb
class Avo::Actions::ReleaseFish < Avo::BaseAction
  self.name = -> { "Release #{record.name}" } 
  self.name = -> { record.present? ? "Release #{record.name}" : "Release fish" } 
end

Then check your system specs: one that opens an association panel's Actions dropdown and runs a non-standalone action without checking a row now finds that action disabled.

Maintaining Previous Behavior ​

An action meant to run from the panel with nothing selected is a standalone action. Set standalone to true. It stays enabled, and handle receives an empty query.

ruby
# app/avo/actions/export_team_members.rb
class Avo::Actions::ExportTeamMembers < Avo::BaseAction
  self.standalone = true
end

avo 4.3.2 Oct 14, 2026 ​

Fields pick up activerecord.attributes and attributes keys

Breaking Change ​

A field with no name: and no translation_key: now reads the model's attribute translation — activerecord.attributes.<model>.<attribute> (parent classes included), then attributes.<attribute> — after Avo's own avo.resource_translations.<resource>.fields.<attribute> and avo.field_translations.<attribute> keys, and before humanizing its id. These are the keys Rails already uses for form labels and error messages, so a localized app gets translated admin labels with no change to its Ruby. See Localizing fields.

Avo's own keys still win, and so do name: and translation_key:. A translation_key: that points at a missing key still falls back to the humanized id, not to the model's translation.

Action Required ​

Grep your locale files for activerecord: and a top-level attributes: key. If you have neither, nothing changes. If you do, every field that was showing a humanized id and matches one of those keys now shows the translation instead. Association fields look up the association name (user), not the foreign key (user_id).

Maintaining Previous Behavior ​

Keep one label as it was with name: or translation_key: on the field, or turn the lookup off for the whole admin:

ruby
# config/initializers/avo.rb
config.model_attribute_translations = false

avo-ai 4.3.0.beta.3 Oct 14, 2026 ​

The retracted chat button renders _pit_core_icon

Breaking Change ​

Hiding the toolbar collapses the chat dock to a single round button, and that button now renders avo/ai/_pit_core_icon instead of avo/ai/_avocado_icon. An app that replaced the assistant's icon with its own _avocado_icon partial keeps its icon everywhere else, but this button goes back to Avo's own mark. See Replace the assistant's icon.

Action Required ​

None, unless you override _avocado_icon. If you do, add a _pit_core_icon partial next to it. Pass classes through and add no size utility of your own: the stylesheet sizes this mark as a share of the button, and a size class in your markup outranks that rule.

erb
<%# app/views/avo/ai/_pit_core_icon.html.erb %>
<%= svg "tabler/outline/robot", class: local_assigns[:classes] %>

avo 4.3.1 Oct 14, 2026 ​

Selected rows come back when you go back to an index

Breaking Change ​

Going back to an index page now puts back the rows you had selected and where you had scrolled. That happens with the browser's Back button and with the Go back link on a record page. Reaching the index any other way, or reloading it, starts with nothing selected, and running an action forgets the selection. Before, Go back opened the index at the top with nothing selected. See Coming back to the index.

Action Required ​

None in production. Check your system specs: one that selects rows, opens a record, clicks Go back and expects nothing to be selected now finds those rows still checked.

Badges follow except_on and show_on: :all on forms

Breaking Change ​

A badge can now render a select on New and Edit. It still stays off forms by default. Before, a badge was forced off forms after its visibility options ran, so except_on: :index or show_on: :all left it display-only there. Now a badge follows those options like every other field, and both put the select on forms. See Editing the value.

Action Required ​

Search your resources for badges declared with except_on or show_on: :all. Each one now shows a select on New and Edit. The select keeps the record's current value, so saving a form doesn't change it.

Maintaining Previous Behavior ​

Add :forms to except_on to keep the badge display-only.

ruby
# app/avo/resources/project.rb
field :status, as: :badge, except_on: :index, options: {success: "done"} 
field :status, as: :badge, except_on: [:index, :forms], options: {success: "done"} 

?set_locale= is stored only for an authenticated request

Breaking Change ​

Avo writes ?set_locale= into Avo.configuration.locale only after your authenticate_with block lets the request through. Before, the write happened in an around_action that ran first, so a request the block rejected still switched the language for every user until the server restarted. A controller that skips Avo's authentication altogether can no longer store it either (#4893).

Action Required ​

None in production. If a link or script outside the admin reached an Avo URL with ?set_locale= while signed out to set the language, it no longer has any effect — set config.locale for everyone, or let each user pick their own with the locale selector.

WARNING

On earlier versions anyone who could reach an Avo URL could change the admin's language for every user, signed in or not. Upgrade rather than working around it.

?set_locale= deprecated, and unknown locales in both locale params are ignored

Breaking Change ​

?set_locale= logs a deprecation warning and will be removed in a future version — it has always changed Avo.configuration.locale for the whole process, so it switches the language for every user until the server restarts. Both ?set_locale= and ?force_locale= now ignore a value missing from I18n.available_locales and move on to the next source. Before, an unknown set_locale was stored globally and made I18n.with_locale raise I18n::InvalidLocale on every later request. See Setting the locale.

Action Required ​

A request carrying a locale your app doesn't list now keeps the configured locale instead of switching, so a spec that asserted a bogus ?force_locale= renders in that locale needs updating. Everything else keeps working; the warning in the logs is the only new signal.

Steps to Update ​

Grep your links, bookmarks and specs for set_locale. To change the language for everyone, set it in the initializer; to let each user choose, use the locale selector, which stores the choice per browser.

ruby
# config/initializers/avo.rb
config.locale = :"pt-BR"

avo-audit_logging 4.3.0 Oct 6, 2026 ​

Activities record where they came from — run the installer again

What changed ​

An activity now carries an origin: origin, a short name for whatever acted, and the optional origin_record pointing at the thing itself. A change made in the panel has none. A change made through an MCP connection, an API token or an Avo AI chat is marked mcp_connection, api_token (or api) and ai_chat, and links back to it — a record's timeline reads Ada Lovelace through MCP connection. Until this version an MCP client's changes were deliberately indistinguishable from the admin's own.

The two columns are a new migration. A first install writes one migration with them included; an app that installed an earlier version has the table without them.

Action Required ​

bash
bin/rails generate avo:audit_logging upgrade
bin/rails db:migrate

upgrade reads your db/migrate and writes only what is missing — it never touches a migration you have already run. Running install again does the same thing.

Until you migrate, nothing breaks: activities keep being recorded, just without an origin, and the Audit log table on a connection's, token's or chat's page is not offered rather than shown empty.

If you have written Avo::AuditLogging::Activity yourself — an importer, a job — you can mark those too; see Mark your own origins.

The audit log is off limits to MCP clients, API tokens and the assistant

What changed ​

Avo::AuditLogging::Activity is no longer offered as a resource through avo-mcp_server's tools, avo-api's routes and entitlements grid, or the Avo AI assistant, whatever your policies say. An agent or a credential that could delete the record of what it just did would make the log worthless.

Action Required ​

Nothing, unless a client of yours was reading /api/resources/avo_activities or asking the assistant about activities — those now answer as if the resource did not exist. Read the audit log in the panel instead.

avo 4.3.0 Oct 6, 2026 ​

avo-api token scopes are now called entitlements

Breaking Change ​

"Scope" already means a policy scope in Avo and an ActiveRecord scope in every app running it, so what an API token is allowed to reach is called an entitlement from 4.3.0 on. 4.2.0 is the only version that shipped it under the old name.

The rename reaches the database column, the panel, the 403 body, and the token policy method:

4.2.04.3.0
avo_api_tokens.scopes columnavo_api_tokens.entitlements
token.scopestoken.entitlements
edit_scopes? on your token policyedit_entitlements?
"reason": "token_scope" in a 403"reason": "token_entitlement"
scopes_grid in a form postentitlements_grid
The Scopes panelThe Entitlements panel

Action Required ​

Re-run the tokens generator and migrate. It writes RenameAvoApiTokenScopes and skips the create migration you already have:

bash
rails generate avo_api:tokens
rails db:migrate

Then grep your app for the left-hand column above. Two are worth checking before the others:

  • edit_scopes? on your token policy. Nothing asks that method any more, so the question falls back to the permissive default and every panel user may edit entitlements. Rename it.
  • A client branching on "reason": "token_scope". It now reads "token_entitlement" and a branch matching the old string falls through.

Nothing grants or revokes anything on its own: the migration renames a column and every stored grant is carried over untouched.

avo-mcp_server 4.3.0.beta.1 Oct 6, 2026 ​

a shipped count_records tool

An app's own count_records tool is no longer served

What changed ​

The server ships a tenth tool, count_records, which answers "how many" with a number and no records. See Tools.

Action Required ​

Nothing, unless the app registers its own tool named count_records through extra_tools, including a subclass of Avo AI's count tool. The shipped tool now takes that name and the app's is dropped with a log line. Give the app's tool another name to keep serving it.

avo 4.2.11 Sep 29, 2026 ​

forms ask before you leave with unsaved changes

warn_on_unsaved_changes is on by default

What changed ​

Avo now asks for confirmation before a user leaves a New or Edit form with unsaved changes. Saving never asks, and a change that was undone does not count. See config.warn_on_unsaved_changes.

Action Required ​

None in production. Check your system specs: one that changes a form and then navigates away without saving now meets the confirmation dialog. There are two ways to handle it:

  • Accept the dialog in those specs with accept_custom_alert from the testing helpers.
  • Turn the warning off in the test environment only, and keep it on everywhere else.
ruby
# config/initializers/avo.rb
Avo.configure do |config|
  # Off in the test environment, on everywhere else.
  config.warn_on_unsaved_changes = !Rails.env.test?
end

INFO

With the warning off in tests, your specs no longer go through what your users see when they leave a form.

Maintaining Previous Behavior ​

Turn it off in every environment.

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.warn_on_unsaved_changes = false
end

avo 4.2.10 Sep 28, 2026 ​

Built-in neutral themes now share one lightness curve

Built-in neutral presets keep their tint but use a consistent surface hierarchy

What Changed ​

Slate, Stone, Gray, Zinc, Neutral, Taupe, Mauve, Mist, and Olive now use Brand's lightness at every shade while preserving their original hue and chroma. Dark primary surfaces are also slightly softer. Navbar text continues to use each named preset's original 300 shade for contrast.

Custom palettes supplied through neutral_colors are unchanged.

Action Required ​

None, unless custom CSS depends on the previous built-in shade values. Review overrides that use --color-avo-neutral-* while a named preset is active, and use the public appearance variables when a surface needs a fixed color.

avo-authorization 4.2.1 Sep 27, 2026 ​

Field authorization

Nothing to add to your policies

What changed ​

A Pundit policy can now name which of a resource's fields a user may reach, with whitelisted_fields and blacklisted_fields, and every Avo surface honors the answer for reads and writes. See Field authorization.

Action Required ​

None. Field lists are opt-in: a policy that declares neither restricts nothing, whether explicit_authorization is on or off. Behavior changes only once a policy declares a list.

Release avo-authorization 4.2.1 together with, or before, avo-ai 4.2.0.beta.6 and avo-mcp_server 4.2.0.beta.9. Both call code that only 4.2.1 ships.

avo-api 4.2.7 Sep 27, 2026 ​

visible:` hides a field from the API too

A field whose visible: proc answers false is no longer serialized

What changed ​

The API used to check only a field's show_on / hide_on options, so a field a visible: proc hid from a user in the panel was still in that user's API payload. It is now left out, the same as in the panel.

A failed create, update or delete no longer names a field a policy withholds from the token's owner in errors. The failure is reported on base instead.

Action Required ​

Nothing, unless an API client reads a field that a visible: proc hides from the token's owner. Make the proc answer true for that user, or the field stays out of the payload.

avo-ai 4.2.0.beta.6 Sep 27, 2026 ​

the chat reads through the resource's fields

A column no field declares is no longer reachable from the chat

What changed ​

The assistant used to read, filter, sort, group and search any column on the model, except password- and token-shaped ones. It now goes through the resource the way the panel does. A column is reachable only when a field the user may reach claims it, and associations are traversed only through an association field. A column no field declares is not returned, filtered, sorted, grouped, searched, written, imported, or named in a schema or an inspector report.

Three things the panel exposes without a field still count:

  • discreet_information: the timestamps and id a resource lists there are readable.
  • created_at: it stays a sort key whenever the column exists.
  • extra_params: the columns a resource opens there are writable.

A model with no Avo resource is never queried or described, and an association check through one is refused.

Action Required ​

Declare a field for every column the chat should work with. A field only one role should see still takes a visible: proc or, with avo-authorization, a policy field list. See How the assistant works.

avo-mcp_server 4.2.0.beta.9 Sep 27, 2026 ​

sorting and errors follow the resource's fields

Clients sort by field columns, and errors name only what the admin may see

What changed ​

  • Sorting: list_records sorts only by a column behind a field the admin may reach, or by created_at. Any other sort_by is refused, with the sortable columns listed.
  • Validation errors: failed writes name only attributes within the admin's reach. This covers the messages, requiredAttributes, optionalAttributes and missingRequiredAttributes. The server log records which attributes were dropped.
  • update_record: its changed list names only readable columns.

Action Required ​

Nothing, unless a client sorts by a column with no field. Declare a field for it. See The MCP server.

An action a client runs sees view as show or index, never new

Breaking Change ​

An action run through run_action used to be built on view: :new. It is now built on the view the panel would have started it from: show when the run targets one record, index for a bulk run or one that takes no records. That is the view its authorize block, its fields and handle read. A bulk run also hands the action no current record, as the panel does. Nothing raises: an action that reads view simply answers differently. See An action runs on the view it would have been started from.

Action Required ​

None for an action that ignores view. Re-read the ones that read it:

  • authorize reading view. -> { view.index? } refused every client run before and now allows a bulk or standalone one. -> { view.new? } allowed every client run and now allows none.
  • fields or handle branching on view.new? took that branch on every client run and now takes it on none. Branch on record, on query.size, or on view.index? instead.

avo-ai 4.2.0.beta.5 Sep 27, 2026 ​

the composer limits what it uploads

Files over 25 MB, and types the assistant can't use, are refused

What changed ​

The chat composer used to upload any file. It now takes images (PNG, JPEG, GIF, WebP), PDFs and text files — markdown, plain text, CSV, TSV, JSON — up to 25 MB, and refuses anything else as it is added, before it uploads, with a line under the draft saying why. A message sent without the composer is held to the same rule: a file it would have refused is left off the message.

Action Required ​

Nothing, unless people upload other files — say, Word documents or videos to attach to records. Allow them under config.ai.uploads; see What the composer accepts.

ruby
config.ai.uploads = {accept: ["*/*"], max_size: 100.megabytes} 

Protect Rails' direct-upload endpoint

What changed ​

Nothing in Avo — this was always true. The composer uploads through POST /rails/active_storage/direct_uploads, which Rails ships without authentication, and the new limits don't cover a request that goes straight to it.

Action Required ​

Add your app's authentication and a size check to that endpoint: Protect the direct-upload endpoint.

records are named by to_param

Records are named by to_param, not their primary key

What changed ​

The assistant names every record the way Avo's own URLs do: by to_param. On a resource that hides its primary keys behind friendly_id, a hashid gem, or its own to_param, the slug now appears in the system prompt, in record labels (Hello world (#hello-world)), in chip and card links, and in the reference the tools hand the model. The tools look an id up through the resource's find_record_method, and still accept a primary key. Their id parameters are strings.

Resources that keep Rails' default to_param see no difference.

Action Required ​

Nothing, unless you replaced part of the assistant with your own copy. Check each of these:

  • An ejected attached_context.txt.erb. Name records by attached_record[:record_param] and each selected record's record_param, not record_id, or your prompt keeps the primary key. See Change how it reads.
  • An ejected write tool (update_record, delete_record, update_records, run_action, active_storage_attachment). The model now passes a slug. Look it up with find_authorized_record_by_param, and declare the id parameter as a string. Diff your copy against the gem's.
  • A tool of your own that returns a reference. Build it from record.to_param. A reference built from record.id on a slugged resource no longer renders as a chip. See Bring your own tool.

An action the assistant runs sees view as show or index, never new

Breaking Change ​

An action the assistant runs used to be built on view: :new. It is now built on the view the panel would have started it from: show when the run targets one record, index for a bulk run or one that takes no records. That is the view its authorize block, its fields and handle read. Nothing raises: an action that reads view simply answers differently. See Your actions, from the chat.

Action Required ​

None for an action that ignores view. Re-read the ones that read it:

  • authorize reading view. -> { view.index? } refused every chat run before and now allows a bulk or standalone one. -> { view.new? } allowed every chat run and now allows none.
  • fields or handle branching on view.new? took that branch on every chat run and now takes it on none. Branch on record, on query.size, or on view.index? instead.

avo 4.2.8 Sep 26, 2026 ​

view inside an action is the page it was started from, never new

Breaking Change ​

Every block of an action (visible, authorize, the modal's texts, fields, and handle) used to see view as new while the modal rendered and while the action ran. It is now the view the action was started from: index, show, or edit. It is never new, and it stays the same from the modal through handle (#4825). See Control visibility and authorization.

Action Required ​

Search your actions for view and drop any branch that expects new:

ruby
# app/avo/actions/toggle_inactive.rb
def handle(query:, fields:, **)
  reload_records if view.new? # never true now 
  reload_records if view.index? # the action was started from the Index page 
end

None if your actions don't read view.

Breaking Change ​

An action's fields used to be filtered by their show_on / hide_on marks against the new view, so a field those marks kept off forms never reached the modal. Those marks describe resource views, so all of an action's fields render now (#4825). A field with no form component, such as badge, renders read-only, as it does on the Show view. See Collect input with fields.

Action Required ​

Look at the actions whose fields carry those marks: the ones that were dropped now show up in the modal. Take a field you don't want there out of the action's fields, or hide it with a visible block.

ruby
# app/avo/actions/toggle_inactive.rb
def fields
  field :status, as: :badge, hide_on: :forms # renders read-only in the modal now 
  field :notify_user, as: :boolean
  field :message, as: :textarea
end

avo-ai 4.2.0.beta.4 Sep 26, 2026 ​

Batch updates are undone as one change: run the installer again

What changed ​

The assistant can now change many records in one confirmation with the new update_records tool, and undo the whole batch from one card. Undo knows which records belong to a batch through a new column, avo_ai_write_logs.pending_write_id. A first install gets it in the create migration; an app that installed an earlier version does not have it.

Action Required ​

bash
bin/rails generate avo:ai install
bin/rails db:migrate

The installer reads your db/migrate and writes only the migrations you are missing, so running it again is safe.

Until you migrate nothing breaks: batch updates still work, but the history lists and undoes a batch record by record instead of as one entry.

A new write tool arrives switched on: update_records

What changed ​

update_records is a new shipped tool, and config.ai.excluded_tools is a list of what to remove, so every app gets it unless it names it. It writes nothing without the user's click on a confirmation card, under the same policies and field rules as update_record.

Action Required ​

Nothing, unless your initializer excludes update_record to keep the assistant from editing records. Exclude the new tool too, or it can still update them in batches:

ruby
config.ai.excluded_tools = [:update_record, :update_records] 

A batch is capped at 50 records. To change that, set config.ai.max_update_records to a number from 1 to 500; anything else raises at boot.

Typing a confirmation with several cards on screen confirms nothing

What changed ​

A typed "do it" used to confirm the last card when one reply had put several on screen. It now confirms none of them and goes to the assistant as an ordinary message, since it cannot tell which card you meant. With one card waiting, a typed confirmation now also still reaches it after the assistant has only reminded you to click.

Action Required ​

Nothing. Click Confirm on the card you want.

avo 4.2.0 Sep 1, 2026 ​

super in a setup_authentication override now accepts a valid API token

Breaking Change ​

avo-api ships API tokens. The default setup_authentication on BaseResourcesController used to raise, closing the API until you replaced it. It now accepts a request carrying a valid API token, authenticated as that token's owner, and rejects everything else.

That changes what super means inside an existing override.

Action Required ​

Grep app/controllers/avo/api/ for setup_authentication and check every override that calls super. An override that never calls it is unaffected.

Drop the super call to opt out of tokens entirely and leave your own scheme in sole charge:

ruby
# app/controllers/avo/api/resources/v1/base_resources_controller.rb
def setup_authentication
  super
  authenticate_with_my_scheme!
end

Keep it to accept tokens alongside your scheme — but note a token then reaches everything its owner reaches, since a token acts as its owner.

avo-api now ships a migration

Breaking Change ​

The tokens table is new, and the token resource appears in the Avo sidebar by default as soon as the gem is upgraded — before the table exists.

Action Required ​

You are already running the API, so your app has the controllers and needs only the table:

bash
rails generate avo_api:tokens
rails db:migrate

Until you do, opening the API tokens screen, or sending any request with an Authorization: Bearer … header, queries a table that isn't there and errors. Running the generator twice is a no-op rather than an error.

Reach for rails generate avo_api:install only on a first install — it invokes avo_api:generate as well, which regenerates every resource controller and asks to overwrite the ones you have customized. avo_api:tokens is the upgrade path and leaves them alone.

If you don't want tokens at all, replace the authentication hook so the token lookup never runs.

A denied policy method answers 403 JSON instead of a 302 redirect

Breaking Change ​

Policy methods (index?, update?, …) returning false raise Avo::NotAuthorizedError, which Avo handled with a flash message and a redirect to your root URL — right for the HTML panel, unreadable for a JSON client. The API now renders JSON instead:

json
{ "error": "Forbidden", "reason": "policy" }

A token scope refusal is also a 403, and reason is what tells the two apart.

Action required: None for most apps — this replaces a redirect no API client could act on. Two cases to check:

  • A client that read the 302 as "denied" now gets a 403. One that followed the redirect and parsed the resulting HTML now gets JSON.
  • A rescue_from Avo::NotAuthorizedError you added to your own BaseResourcesController still wins over the gem's, so your response shape is unchanged. Delete it to adopt the gem's, or keep it.

Tokens can now be scoped, and are unrestricted until they are

Breaking Change ​

None — this is additive. Existing tokens carry no grants, and a token with no grants reaches everything its owner can, exactly as before.

Action required: None. Worth knowing before you use it: granting a token anything restricts it to what you granted, including against resources you deploy later, and removing the last grant leaves a token that refuses everything rather than one that is unrestricted again. Make unrestricted in the token's panel is the way back.

Editing grants is gated by a policy method, which — like every other token policy method — answers yes when no authorization client is configured.

Renamed in 4.3.0

This shipped as Scopes, with an edit_scopes? policy method. Both are called entitlements from 4.3.0 on — see Upgrade to 4.3.0.

avo 4.1.15 Aug 20, 2026 ​

container_width values renamed to Tailwind's scale

Deprecation ​

config.container_width (and self.container_width on Avo Forms pages and forms) used the words :large and :small, while every other size option in Avo uses Tailwind's scale — size: :sm, width: :xl, size: :md. The widths now use that scale too:

BeforeAfter
:large:lg
:small:md
:full:full (unchanged)

:small became :md rather than :sm deliberately: it keeps every existing screen at the width it already had, and frees the narrower names. A new :sm (720px, clamping from 768px) ships in the same release for single-column screens — nothing gets it automatically, it is opt-in.

Action Required ​

None immediately. The old names are still accepted and mapped for you — they log a deprecation warning through Avo.logger and will be removed in Avo 5. Update them when convenient:

ruby
# config/initializers/avo.rb
config.container_width = :small
config.container_width = :md

config.container_width = { index: :large }          
config.container_width = { index: :lg }             
ruby
# app/avo/pages/settings.rb
self.container_width = :large
self.container_width = :lg

If you style Avo's container in your own CSS, this part is not aliased — the emitted class names changed and your overrides need renaming:

BeforeAfter
.container-large.container-lg
.container-small.container-md
.container-full-width.container-full

avo 4.1.11 Aug 17, 2026 ​

Breaking Change ​

A field declared stacked: false used to be ignored in the places Avo stacks on its own — inside a sidebar, in the record preview, and on any field with a width below 100. The field's own declaration now wins over those defaults, so such a field renders inline. See stacked.

Action Required ​

If you never pass stacked: false, nothing changes — the defaults are untouched.

If you do, the fields that carry it move from stacked to inline. Drop the option to keep them stacked.

ruby
# app/avo/resources/user.rb
field :name, as: :text, stacked: false
field :name, as: :text

avo-calendar 4.0.3 Aug 16, 2026 ​

+N more no longer opens the week view

In the month view, the +N more toggle under a crowded day now expands that week row in place to reveal every event, with a Show less control to fit it back. It previously navigated to that day's week view. See Read a crowded day.

Action required: None — the behavior change is visual only, and the week view stays one click away in the header's Month/Week toggle.

Week view becomes a scrollable time grid

Hour-based events now stretch to their duration — overlapping events share the day column side by side — instead of rendering as fixed chips on their start hour, and the grid scrolls inside the viewport, opening at 07:00 under a pinned day-header row. See Month and week views.

Action required: None — the change is visual only. An event without an ends_at renders as a one-hour block.

avo 4.1.10 Aug 15, 2026 ​

code and easy_mde collapse their content on Show

The code and easy_mde fields now render on the Show view through the same collapsible wrapper the rich text fields use: the value is clipped to a short, faded preview with a More content link that expands it and a Less content link that collapses it back. They previously rendered the full value.

Action required: None unless you want the full value visible on load. Pass always_show: true on the field to skip the collapsing:

ruby
# app/avo/resources/product.rb
field :custom_css, as: :code, always_show: true

See WYSIWYG & Markdown editors for the behavior every editor field now shares.

Editors render in a bounded, resizable viewport on forms

On Edit and New, every editor field (trix, rhino, lexxy, markdown, easy_mde, tip_tap and code) now renders inside a viewport that opens 20rem tall, scrolls its overflow, and can be dragged taller or shorter by its bottom-end handle. Editors previously grew with their content. Each height is stored in the browser's local storage and restored the next time that form renders.

Action required: None. If the default size doesn't suit your content, set the heights globally in a stylesheet you register with Avo:

css
/* app/assets/stylesheets/avo_custom.css */
:root {
  --avo-resizable-editor-default-height: 30rem; 
  --avo-resizable-editor-min-height: 15rem; 
}

Note that the code field's height option now only applies on the Show view — on forms the viewport's height wins.

avo 4.1.7 Aug 13, 2026 ​

Breaking Change ​

Avo::MediaLibrary.configuration.visible used to hide the sidebar item and the rich text editor button while leaving every Media Library route reachable by URL. It is now enforced on the routes as well: a user the block returns false for gets a 404 from /media-library and /attach-media.

This closes GHSA-cff8-4h3c-9r4q, where any authenticated Avo user could browse, rename and permanently delete every Active Storage blob in the application.

Action Required ​

If you set config.visible, check that it returns true for everyone who is meant to use the Media Library, including through the rich text editors. Anyone it rejects now loses access entirely rather than only losing the menu item.

If you don't set it, nothing changes on upgrade — visible defaults to true. That also means the Media Library stays available to every user who can sign in to Avo, so set it if that isn't what you want:

ruby
# config/initializers/avo.rb
if defined?(Avo::MediaLibrary)
  Avo::MediaLibrary.configure do |config|
    config.visible = -> { Avo::Current.user.is_developer? }
  end
end

Migrating to TailwindCSS 4? See the TailwindCSS 4 Migration Guide.

avo-dashboards + avo-scopes 4.1.2 Aug 11, 2026 ​

Cards, dashboards, and scopes resolve their labels from locale keys first

Breaking Change ​

Cards, dashboards, and scopes now derive an i18n key from their class path and look it up before falling back to the class attribute, the same way resources and actions already did:

ClassKey Avo looks upOptions under it
Cardavo.card_translations.<class_path>label, description, discreet_description
Dashboardavo.dashboard_translations.<class_path>name, description
Scopeavo.scope_translations.<class_path>name, description

If your locale files already define one of those keys, it now wins over self.label, self.name, or self.description — including when the attribute is a lambda. The lambda isn't called at all, so anything it computed is dropped, with nothing in the logs.

A lambda that reads the same key stops running

This bites hardest when the attribute builds a string around the very key Avo now derives:

ruby
# app/avo/cards/upcoming_metric.rb
self.description = -> {
  I18n.t("avo.card_translations.upcoming_metric.description") + " (#{timeframe})"
}

Avo finds avo.card_translations.upcoming_metric.description, renders it verbatim, and the (#{timeframe}) suffix disappears.

Action Required ​

Grep your locale files for card_translations, dashboard_translations, and scope_translations. Avo ships nothing under those roots, so if you weren't using them there's nothing to do — a missing key still falls through to your class attribute exactly as before.

If you were using them for your own I18n.t lookups, pick one of the three ways out below.

Steps to Update ​

1. Move your own key out of the derived namespace. The cleanest fix when the class attribute has to keep computing something:

yaml
# config/locales/avo.en.yml
en:
  avo:
    card_translations:
      upcoming_metric:
        description: Forecasted revenue based on current bookings
        description_text: Forecasted revenue based on current bookings
ruby
# app/avo/cards/upcoming_metric.rb
self.description = -> {
  I18n.t("avo.card_translations.upcoming_metric.description") + " (#{timeframe})"
  I18n.t("avo.card_translations.upcoming_metric.description_text") + " (#{timeframe})"
}

2. Point Avo's lookup somewhere else. self.translation_key moves the derived key and leaves your own I18n.t call untouched:

ruby
# app/avo/cards/upcoming_metric.rb
class Avo::Cards::UpcomingMetric < Avo::Cards::MetricCard
  self.translation_key = "avo.card_translations.upcoming_metric_chrome"
end

3. Override at the registration site. A value passed to card beats the locale key, so one registration can keep a computed description while the others stay translated:

ruby
# app/avo/dashboards/dashy.rb
card Avo::Cards::UpcomingMetric, description: -> { "#{I18n.t("...")} (#{timeframe})" }

Actions have no registration site, so only the first two apply there — see Upgrade to 4.0.17.

See the key map for every key Avo derives.

avo 4.1.4 Aug 10, 2026 ​

The back to top pill is enabled by default

Breaking Change ​

The "Back to top" pill used to be off by default and only revealed itself when the user scrolled back up. It's now on by default and its visibility depends only on how far down the page is scrolled, not on the scroll direction. The threshold default moved from 64 to 400 pixels so the pill stays out of the way on short scrolls.

Action required: None, unless you don't want the pill in your app. Every app that doesn't configure back_to_top gets it after this upgrade (#4705).

Maintaining Previous Behavior ​

Turn it off from the initializer. Keys you leave out fall back to the defaults, so enabled is enough:

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.back_to_top = {enabled: false} 
end

The direction-aware reveal is gone for good — threshold is now the only thing that decides when the pill shows up.

use_browser_timezone now defaults to true

Breaking Change ​

Server-side dates and times now render in each visitor's own time zone by default instead of the app's Time.zone. Avo detects the browser's zone via a cookie; the first page a browser loads soft-reloads once through Turbo and shows a one-time alert that times are now displayed in the visitor's zone.

Action required: None if per-visitor times are what you want — most apps do. Displayed values only change for users whose browser zone differs from the app zone; nothing about stored data changes.

Since 4.1.8 the option defaults to false in the test environment, so browser/system tests are unaffected — the first-load soft reload would otherwise race them. On 4.1.4 through 4.1.7 it is on in test as well; set config.use_browser_timezone = !Rails.env.test? if you stay on one of those and your system specs started flaking.

Maintaining Previous Behavior ​

Pin every visitor to the app's configured zone:

ruby
# config/initializers/avo.rb
Avo.configure do |config|
  config.use_browser_timezone = false
end

avo-dashboards 4.0.9 Jul 28, 2026 ​

The per-card refresh control is opt-in

Breaking Change ​

avo-dashboards 4.0.8 rendered a refresh control on every card, with no way to remove it — including on html and static partial cards, where refreshing re-renders identical content. The control is now off by default and turns on per card with refresh_button, the same name and the same default as the dashboard-wide control that shipped alongside it.

Action required: None unless you were on 4.0.8 and want the control kept on a card.

Maintaining Previous Behavior ​

Turn it back on for each card that should keep it:

ruby
# app/avo/cards/users_metric.rb
class Avo::Cards::UsersMetric < Avo::Cards::MetricCard
  self.id = 'users_metric'
  self.refresh_button = true
end

Setting it on a dashboard does not turn it on for that dashboard's cards — the two controls are independent opt-ins under the same name.

avo 4.0.20 Jul 28, 2026 ​

Breaking Change ​

The sidebar is now resizable, and as part of that change sidebar link, section, and group labels render on a single line with an ellipsis when they overflow, instead of wrapping onto multiple lines. The full label shows in a tooltip on hover.

Action required: None for most apps. If your navigation relied on long labels wrapping, either shorten the labels or point users at the drag handle to widen the sidebar.

Hosts with an accessibility conformance obligation can disable the drag handle. The off-switch was config.sidebar_resizable on 4.0.20 and 4.0.21; from 4.0.22 it moved into the grouped config as sidebar[:resizable] and the old name no longer works.

.container-small is now max-width-based

Breaking Change ​

.container-small (the wrapper around show and edit pages) used a fixed width; it now fills its container up to a max-width so content adapts when the sidebar is wide. If you override .container-small's width from your own stylesheets, the new Avo-owned max-width now clamps it — override max-width instead.

Action required: None unless you override .container-small.

avo 4.0.18 Jul 23, 2026 ​

Resource and field translations are used verbatim

Breaking Change ​

Avo used to humanize every resource and field label it resolved from your locale files. That silently overrode the casing you wrote: 'Payment Intent ID' rendered as Payment intent id, and 'API Products' rendered as Api products.

Avo now renders a resolved translation exactly as written and humanizes only the name it generates for you when no translation is found.

Lowercase translations now render lowercase

If your locale entries are lowercase, they used to appear capitalized. They will now appear as written.

Action Required ​

Review your locale files. If every resource_translations and field_translations entry is already written the way you want it on screen, there's nothing to do.

Avo's own interface strings are unaffected — the "New", "Edit", and "View" labels still render as before.

Steps to Update ​

Grep your locale files for resource_translations and field_translations, and capitalize any entry you want capitalized on screen:

yaml
# config/locales/avo.pt-BR.yml
pt-BR:
  avo:
    resource_translations:
      user:
        zero: 'usuários'
        one: 'usuário'
        other: 'usuários'
        zero: 'Usuários'
        one: 'Usuário'
        other: 'Usuários'
    field_translations:
      file:
        zero: 'arquivos'
        one: 'arquivo'
        other: 'arquivos'
        zero: 'Arquivos'
        one: 'Arquivo'
        other: 'Arquivos'

Nothing else changes: resources and fields without a translation still fall back to the humanized class or attribute name.

See Localization (i18n) for the full picture.

avo 4.0.17 Jul 23, 2026 ​

Action labels resolve from avo.action_translations first

Breaking Change ​

Actions now look up avo.action_translations.<class_path>.{name,message,confirm_button_label,cancel_button_label,description} before falling back to self.name, self.message, self.confirm_button_label, self.cancel_button_label, and self.description, mirroring how resources already resolved their names.

If your locale files already define one of those keys, it now wins over the class attribute — including when the attribute is a lambda, which is then never called, so anything it computed is dropped with nothing in the logs.

Action Required ​

Grep your locale files for action_translations. Avo ships nothing under that root, so if you weren't using it nothing changes: an action with no key still renders its class attribute, and an action with neither still renders its humanized class name.

Steps to Update ​

If a key you own collides with an action's derived key, either rename your key, or point Avo's lookup elsewhere with self.translation_key:

ruby
# app/avo/actions/toggle_inactive.rb
class Avo::Actions::ToggleInactive < Avo::BaseAction
  self.translation_key = "avo.action_translations.toggle_inactive_chrome"
end

avo-dashboards and avo-scopes got the same treatment in their 4.1.2 releases — see that section.

See Localizing actions for the full picture.