Has And Belongs To Many
The has_and_belongs_to_many field displays related records in an association table and lets users create, attach, and detach them. It works similarly to has_many, but the Rails association stores links in a join table without a separate join model.
# app/avo/resources/team.rb
class Avo::Resources::Team < Avo::BaseResource
def fields
field :users, as: :has_and_belongs_to_many
end
endWith no extra options, Avo infers the related resource, uses a select input for attaching records, and lazy-loads the table. The field appears on the configured default resource view (normally Show), not as an editable form input. Configure the Rails association and join table first; see the associations overview.
Choose records to attach
For large collections, enable searchable and follow the Searchable associations guide to configure the target resource's search query. For selecting several loaded records at once, use attach_using instead:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many, attach_using: :checkbox_listThe checkbox list includes client-side search over the loaded records and respects the association lookup limit. Configure the target resource's self.search[:item] to add row titles, descriptions, images, or avatars, using the same presentation as the CheckboxList field.
If only some users may be attached, narrow attach_scope. For example, exclude users already on the team:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many,
attach_scope: -> { query.where.not(id: parent.users.select(:id)) }Build on query to retain the user's policy scope. The attach save accepts only records returned by this scope. This does not filter the existing association table.
Filter the displayed records
Watch the demo video Use scope to narrow the records shown in the panel, independently of the attach picker:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many,
scope: -> { query.where(active: true) }The relation already includes the associated resource's policy scope. The callback also exposes parent, resource, and parent_resource; see the API for their meanings.

React to attaches and detaches
Sinceavo >= v4.3.11Use before_attach or before_detach to reject a change, and after_attach or after_detach for notifications or audit entries:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many,
after_attach: -> {
Rails.logger.info "#{current_user&.id} attached user #{record.id} to team #{parent.id}"
}The before hooks run inside the change's transaction. Raise a regular error to reject the change; returning false does not stop it. After hooks run after commit, so their errors cannot undo the change. Detaching removes the join row, not the associated user.
WARNING
These hooks run only through the association's Attach and Detach controls, not through nested forms, Create, Delete, actions, or model code. Do not raise ActiveRecord::Rollback: Rails swallows it and the reason is not shown. Rescue errors in after hooks if a failed side effect should not fail the response.
Customize the panel
Give the table a clearer name and description:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many,
name: "Team members",
description: "Users who can access this team."

Set per_page for this table's page size, or discreet_pagination to hide pagination details when there is only one page. Hide table controls with hide_search_input and hide_filter_button. These controls are separate from the attach picker's search.
Enable linkable to open the association table on its own page in a new tab.

Defer or refresh the table
Set loading to manual if the table is expensive and users do not always need it:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many,
loading: { mode: :manual, auto_load_for: 5.minutes }The placeholder offers a Load button and an inline Retry button if the fetch fails. Once opened, the frame is remembered in a per-record, per-frame cookie; return visits within the sliding window refresh that window. Set auto_load_for: 0 or nil to show the placeholder on every visit. See global association defaults.
Keep descriptions cheap
The description also renders on the manual placeholder. Avoid querying the database there, or branch on loading_type:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many, loading: :manual,
description: -> { loading_type == :manual ? "Team members" : "#{query.count} members" } Enable reloadable for an icon that refreshes only the table. If deleting an associated record changes a parent counter or total, use refresh_on_destroy to refresh the entire page after deletion.

Choose a different resource or association
Set use_resource when the related model has more than one Avo resource. For STI links, link_to_child_resource chooses the child resource when available; see STI associations.
Use for_attribute to show the same Rails association under multiple field IDs, for example with separate scopes:
# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many
field :active_users, as: :has_and_belongs_to_many,
for_attribute: :users, scope: -> { query.where(active: true) }Show the table on edit screens
Add show_on: :edit to render the association table on Edit as well. This is still the Show component, not a nested form. See shared field visibility options.
Edit related records in the parent form
For creation and editing in the same form, use nested. This requires the avo-nested gem and Rails nested attributes:
# Gemfile
gem "avo-nested", source: "https://packager.dev/avo-hq/"Run bundle install; see Gem server authentication if the private source is not configured.
# app/models/team.rb
class Team < ApplicationRecord
has_and_belongs_to_many :users
accepts_nested_attributes_for :users, allow_destroy: true
end# app/avo/resources/team.rb
field :users, as: :has_and_belongs_to_many,
nested: { on: :forms, limit: 5 }The nested form uses the child resource's edit fields. The limit hides Add when reached. nested: true enables both New and Edit; use on: :new or on: :edit for only one.
With avo-nested >= 4.3.3, existing rows offer Delete only when allow_destroy: true. Delete asks for confirmation and marks the row pending with Undo until the parent form is saved. Saving removes only the join row; the user record is kept. Unsaved rows can always be removed. Translate these messages through avo.nested.mark_for_deletion_confirmation and avo.nested.pending_deletion; see i18n.
Authorize nested changes
With authorization enabled, existing records must be inside the child's policy scope. Updates require the child's update? both before and after assigning submitted values: a refusal before assignment skips the row, while a refusal after assignment prevents saving. Deletions require destroy?, and refusal prevents saving. New rows require both the child's create? and the parent's create_users?; refusal prevents saving. The parent policy also controls whether Add is visible. With explicit authorization enabled, define create_users? on the parent policy to permit new nested rows. See authorization and the nested forms guide.
Control association buttons
Use association policy methods to control Attach, Detach, Create, Destroy, and action buttons. Hiding a button is not a replacement for authorization.

Options reference
See the Has And Belongs To Many API for all association-specific options and callback locals. General options such as visibility and labels are covered in shared field options.