| name | avo-associations |
| description | Wire Active Record relationships into Avo resources as association fields (`field :x, as: :belongs_to | :has_one | :has_many | :has_and_belongs_to_many`) inside `def fields` in `app/avo/resources/<model>.rb`, including polymorphic and `has_many :through` relations, type-to-search pickers, extra join-table fields at attach time, and nested create-in-form. Use when the user wants to link one record to another or surface related records — whether they say it in Avo terms ("add a belongs_to field", "show a has_many panel", "make the association searchable", "polymorphic belongs_to", "has_many through with attach_fields", "use_resource / attach_scope on an association") or in plain Rails/product terms with no Avo vocabulary: "a post belongs to a user", "show all of a user's orders on their page", "add a profile to the user", "connect users and teams with a join table", "the user dropdown is too long — make it type-to-search", "a comment can belong to a post or a project", "add extra fields when attaching a member to a team", "create the related record right from the form", "let admins pick the author while editing the post", or "link these two models in the admin". |
| allowed-tools | Read, Edit, Write, Glob, Grep, Bash, WebFetch |
| metadata | {"requires-gem":"avo-advanced_search (https://avohq.io/addons/searchable-associations) — only for searchable associations; the rest is Community"} |
Avo Associations
Avo turns Rails Active Record associations into fields. You declare one line in the resource's def fields and Avo renders it per view: a belongs_to becomes a link on Show/Index and a dropdown (or search picker) on the forms; has_one, has_many, and has_and_belongs_to_many render as panels below the record's fields with attach / detach / create controls.
These fields live in app/avo/resources/<model>.rb, the same place as every other field. This skill is where relationship-shaped requests land — "a post belongs to a user", "show a user's orders", "connect users and teams", "make the picker type-to-search". For non-association fields (text, number, select, file…) use avo-fields; for creating or configuring the resource shell itself use avo-resources; for who can attach/detach/create use avo-authorization.
Docs (fetch on demand with WebFetch — verify option names and edge cases against the live pages, don't guess):
When this applies
Use this skill when the request is about one record relating to another, in any phrasing:
- Direct Avo asks: "add a
belongs_to", "show a has_many panel", "make it searchable", "polymorphic belongs_to", "has_many :through with attach_fields".
- Rails-shaped asks without Avo words: "a post belongs to a user", "a user has one profile", "show all of a user's orders on their page", "connect users and teams with a join table".
- Product-shaped asks: "the author dropdown is too long — let me type to search", "a comment can belong to a post or a project", "add a rating when I attach a member to a team", "create the related record right from the form".
If the request is about a scalar attribute (a string, a number, a file, an enum select) rather than a link between records, it's an avo-fields task, not this one.
Workflow
-
Confirm the Rails association exists. Association fields are a view of an Active Record association — Avo does not create it. Check the model (app/models/<model>.rb) for the matching belongs_to / has_one / has_many / has_and_belongs_to_many (and the inverse on the other model). If it's missing, add it to the model first, and set inverse_of on both sides.
-
Pick the field type from the Association types table — it mirrors the Rails macro one-to-one.
-
Add the field to def fields in app/avo/resources/<model>.rb:
class Avo::Resources::Post < Avo::BaseResource
def fields
field :id, as: :id
field :title, as: :text
field :user, as: :belongs_to
field :comments, as: :has_many
end
end
The field name is the association name, not a column — field :user maps to belongs_to :user (the user_id column is implicit). A has_many field's counterpart lives on the other resource: Post has_many :comments is field :comments, as: :has_many on the Post resource, and the reciprocal field :post, as: :belongs_to on the Comment resource.
-
Layer options as needed (see ) — scope the attach list, point at a different resource, make it searchable, allow nested creation, etc.
Association types
| Rails association on the model | Avo field declaration |
|---|
belongs_to :user | field :user, as: :belongs_to |
belongs_to :commentable, polymorphic: true | field :commentable, as: :belongs_to, polymorphic_as: :commentable, types: [::Post, ::Project] |
has_one :admin | field :admin, as: :has_one |
has_many :comments | field :comments, as: :has_many |
has_many :members, through: :memberships | field :members, as: :has_many, through: :memberships |
has_and_belongs_to_many :teams | field :teams, as: :has_and_belongs_to_many |
has_one, has_many, and has_and_belongs_to_many render only on the Show view by default; add show_on: :edit to also surface them on the edit form (still rendered with the show-view component — for editable-in-form use nested).
Key options
Most options are shared across the association fields; a handful are type-specific. Confirm exact behavior against the linked docs before relying on an edge case.
Shared by all association fields
| Option | What it does |
|---|
searchable | Replaces the <select> / attach picker with a type-as-you-search input. Paid add-on + needs a query source — see Searchable. |
attach_scope | -> { query.non_admins } — scopes the records offered in the attach modal / dropdown (locals: query, parent). Note: does not filter the listed rows of a has_many/HABTM — use scope or a Pundit policy scope for that. |
use_resource | Render/redirect through a different resource, e.g. use_resource: Avo::Resources::PhotoComment (class or string). |
name | Overrides the panel/label text (has_many, HABTM). Also achievable via field i18n. |
description | Sub-title text under the panel title. Keep it cheap under loading: :manual — the lambda runs on the placeholder too. |
scope | -> { query.approved } — scopes the rows displayed in a has_many/HABTM table (locals: query, parent, resource, parent_resource). |
loading | :manual defers the frame fetch behind a Load button (heavy associations); :lazy is the default. |
linkable | Makes the panel title open the association table on its own page. |
reloadable | Adds a reload icon on the panel (boolean or a lambda gated on e.g. current_user.admin?). |
nested | Create/edit the related record inline in the parent form. Requires the avo-nested gem. nested: true = { on: :forms }; caps rows on /HABTM. |
belongs_to-only
| Option | What it does |
|---|
polymorphic_as: + types: | Turns the field polymorphic. polymorphic_as: :commentable, types: [::Post, ::Project] — the two must be used together, matching a belongs_to :commentable, polymorphic: true on the model. Renders two dropdowns: pick the type, then the record. |
polymorphic_help | Help text for the type dropdown (use help: for the record dropdown). |
can_create | true/false — show/hide the "create new" link on the form. Overridden by the target resource's create? policy (see Gotchas). |
allow_via_detaching | Keeps the field editable when you reached the record through that association (which otherwise disables it). |
link_to_record | true makes the Index cell link to the current row's record instead of the associated one. |
has_many :through and attach_fields
For a join model with its own columns, expose those columns at attach time with attach_fields (a lambda that declares fields, evaluated against the join model):
field :members,
as: :has_many,
through: :memberships,
attach_fields: -> {
field :role, as: :text
}
The extra fields render inside the attach modal and their values persist on the join row. attach_fields only persists on has_many :through — it exists but is a no-op on a plain has_many / HABTM. If the through model is polymorphic, add the type as a hidden field: field :membership_type, as: :hidden, default: "TheType".
Searchable associations
When the target has too many records for a dropdown, searchable swaps it for a type-to-search picker.
- Paid add-on (
avo-advanced_search gem) — searchable-associations. Confirm the license before proposing it.
- Needs a query source, or the picker is silently empty. Supply it either on the target resource (
self.search = { query: -> { ... } }, shared by every picker pointing at it) or field-level inside searchable: { query: -> { ... } } (that picker only; overrides the resource-level one).
Boolean form — reuse the target resource's self.search:
field :links, as: :has_many, searchable: true
Hash form — override per picker, or gate it:
field :user, as: :belongs_to,
searchable: {
query: -> { query.ransack(name_cont: q).result(distinct: false) },
item: -> { { title: "Reviewer: #{record.first_name}", description: record.email } },
enabled: -> { current_user.admin? }
}
Polymorphic belongs_to runs the search once per declared type. With the boolean form, define self.search[:query] on each target resource. With a single field-level proc, branch on query.klass (or query.klass.name):
field :commentable, as: :belongs_to,
polymorphic_as: :commentable, types: [::Post, ::Project],
searchable: {
query: -> {
case query.klass.name
when "Post" then query.ransack(body_cont: q).result(distinct: false)
when "Project" then query.ransack(name_cont: q).result(distinct: false)
end
}
}
query: locals include q, query, params, parent_record (can be nil on create forms — guard with &.); item: locals are record, resource.
Gotchas
- Set
inverse_of on the model association. Avo relies on it to resolve the reciprocal; missing it causes wrong/empty attach lists and save bugs. Set it on both sides.
- The Rails association must exist first. Adding
field :x, as: :has_many does nothing if the model has no has_many :x.
has_* fields are hidden on Edit by default. Add show_on: :edit to surface them on the form. For editing the related record in the form (not just displaying it), use nested — which needs the avo-nested gem.
- Attach/detach/create/destroy buttons come from the target resource's Pundit policy, and the method names are plural, matching the association name:
attach_users?, detach_users?, create_users?, destroy_users?, view_users?, show_users? — not the singular detach_user?. This is the #1 "why isn't my button showing" cause. Cross-link avo-authorization.
can_create: true is still vetoed by the policy. If the target resource's create? returns false, no create link appears regardless of can_create.
- A
belongs_to field is disabled when you arrive through that same association. Editing a Comment reached via a Post's comments panel disables the post field. Set allow_via_detaching: true to re-enable it.
polymorphic_as and types are a pair — using one without the other raises. types is an array of model classes ([::Post, ::Project]), matching a polymorphic: true belongs_to.
attach_scope does not filter listed rows, only the attach picker. To hide rows from a has_many/HABTM table, use scope or a Pundit policy scope.
- STI child resources must set
self.model_class. When a parent class and STI children share a table, give each child resource so Avo maps it correctly. To route association rows / index clicks to the child resource instead of the parent, use on the parent resource or on the field.
Report
After wiring the association, tell the user:
- The field(s) added and to which resource file (absolute path), with the
as: type.
- Any model change you made or that's still required (the Rails association,
inverse_of), since Avo only renders the association.
- Options applied and why (searchable, scope, nested,
attach_fields, polymorphic, etc.).
- Any paid add-on dependency introduced (
searchable → advanced-search; nested → avo-nested gem) and whether it still needs installing / a query source.
- If button visibility matters, a pointer that attach/detach/create/destroy are governed by the target resource's plural policy methods — hand off to
avo-authorization if changes are needed.