Skip to main content
iVentureTeam

many2many

The default face of a many2many field is an embedded table of linked records with an Add line that opens a selection dialog. Less known: writing widget="many2many" on a one2many field flips it into link and unlink mode, a trick Odoo core itself uses.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 20, 2026Updated August 20, 20266 min read
Studio nameMany2Many (default display of the field type)
Technical namemany2many
Field typesmany2many, one2many
Viewsform (embedded list or kanban), list (as plain text via list.many2many)
Also registered aslist.many2many (renders the count as plain text in list cells)
Moduleweb, present in every Odoo database
Used in core4 explicit occurrences across 4 modules, including l10n_id_efaktur_coretax, gamification, stock_picking_batch; implicitly used by every many2many field without a widget
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0, Odoo 17.0, Odoo 16.0
No-code setupYes. Studio applies it automatically when you add a Many2Many field.
Alternativesmany2many_tags, many2many_checkboxes, one2many, many2many_binary

What the Many2many table widget does

When a many2many field appears in a form without any widget attribute, this is what renders: X2ManyField, the embedded relational table registered under both one2many and many2many. It embeds a real list or kanban view of the linked records (the mode attribute picks which), with a pager, inline editing when the subview allows it, and an Add line.

The many2many personality differs from one2many in three ways, all in the source. First, Add opens a select and create dialog (titled Add: plus the field label) instead of a new inline row, and the dialog's search domain excludes records that are already linked. Second, removing a line calls forget, which drops the relation but never deletes the target record. Third, the Add button obeys the link option when present and only falls back to create.

The mode switch is a one line check: field.type === "many2many" || props.widget === "many2many". That second half means the widget name alone can force many2many behavior onto a one2many field.

What this means for your team

The embedded table is where teams manage real relations: pickings in a batch transfer, badge ranks, tax documents. The unlink not delete semantics matter operationally, removing a picking from a batch must not delete the picking, and this widget guarantees that by construction.

The domain capable crud options are the governance tool. A rule like allow adding lines only while the parent is in draft is one option value, no code: options="{'link': [('state', '=', 'draft')]}". That keeps data discipline in the view layer where auditors can read it.

Setting it up in Odoo Studio (no code)

Studio applies this widget automatically; you never pick it by name:

  1. Open the form in Studio.

  2. Drag a Many2Many field from the component palette onto the form.

  3. Choose the target model for the relation and save.

  4. The field renders as this embedded table. Use the field's properties to disable creation or linking if needed.

What Studio cannot do here

Studio cannot express the domain form of the crud options; its toggles produce plain booleans only. Conditional rules like unlink only in draft still require editing the XML. Studio also cannot switch a one2many field into many2many behavior; that trick is XML only.

Supported options in Odoo 19

The interesting options are the five crud keys consumed by useActiveActions. Each accepts true, false, or a domain evaluated against the parent record's context, a capability no Studio toggle exposes. The component also merges them with the embedded subview's own create/delete/edit arch attributes, and the stricter side wins.

OptionTypeWhat it does
createboolean or domainAllows creating new target records from the widget. A domain is evaluated against the parent record. For the Add button in many2many mode it is only the fallback: link wins when present. Becomes an XML attribute on the Odoo 20 development branch.(default: true)
linkboolean or domainGoverns the Add button in many2many mode (takes precedence over create). A domain is evaluated against the parent record, enabling rules like linking only while draft. Becomes an XML attribute on the Odoo 20 development branch.(default: true)
unlinkboolean or domainControls the per row remove action in many2many mode, which drops the relation without deleting the record. Becomes an XML attribute on the Odoo 20 development branch.(default: true)
deleteboolean or domainControls actual record deletion, relevant in one2many mode; in many2many mode removal goes through unlink instead. Becomes an XML attribute on the Odoo 20 development branch.(default: true)
writeboolean or domainNew in 19: gates inline editing of embedded rows. When false or when its domain fails, the renderer flips readonly even if the subview is editable. Becomes an XML attribute on the Odoo 20 development branch.(default: true)(since Odoo 19.0)

Two traps from the source: the edit key is overwritten internally with the record's edition state, so passing it has no effect, and m2o style options like no_create are never read by this widget. On the Odoo 20 development branch all five keys move from options="{...}" to plain XML attributes.

Working examples

A standard many2many with linking restricted while draft, and creation disabled entirely:

<field name="tax_document_ids" widget="many2many"
       options="{'link': [('state', '=', 'draft')], 'create': False}"/>

The stock_picking_batch trick: a one2many field rendered with many2many behavior, so removing a picking unlinks it from the batch instead of deleting it:

<field name="picking_ids" widget="many2many" mode="list,kanban"
       readonly="state not in ['draft', 'in_progress']"/>

Renaming the Add line with the add-label attribute:

<field name="rank_ids" widget="many2many" add-label="Add a rank"/>

Dialog domains, the write gate and the list cell variant

Details from the 352 line source that decide real world behavior:

The selection dialog excludes only saved links. The domain sent to the dialog filters out currentIds that are numbers, so records linked but not yet saved (virtual ids) are not excluded. In practice you will not see duplicates because saving normalizes the commands, but the search can transiently show a record you just linked.

write gates inline editing, and it is new in 19. The renderer receives readonly: props.readonly || !activeActions.write, so options="{'write': False}" (or a failing domain) makes embedded rows read only while still allowing link and unlink. The 18 source has no such gate.

Subview attributes AND with options. If the embedded list arch says create="false", no option can re enable creation; useActiveActions combines both and the stricter answer wins.

The list.many2many variant is a different component. In list view cells the registry serves ListX2ManyField, which prints formatX2many's plain text count (for example 3 records) and loads no subview at all. That is why a many2many column in a list view shows a count instead of tags unless you set widget="many2many_tags".

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. The crud options move from options to XML attributes in the development branch; see below.
Odoo 19.0VerifiedVerified against the shipped source and tested on a clean database; write gating is new here.
Odoo 18.0VerifiedSame component and options except the write gate, which does not exist yet.
Odoo 17.0VerifiedSame registration and crud option handling.
Odoo 16.0VerifiedSame registration; domain capable crud options already supported.

Views migrate cleanly from 16 through 19. Note that options="{'write': ...}" only has an effect from 19 onward; on 18 and earlier the inline rows follow the subview's own editability alone.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. The changes below are read from the public development branch and are not final until release.

The crud options become attributes. On master, extractProps builds crudOptions with pick(attrs, "create", "delete", "link", "unlink", "write"); the options dictionary is no longer read. Every options="{'link': ..., 'create': ...}" on an x2many field will need rewriting as link="..." and create="..." attributes after the upgrade, across every custom view.

Kanban internals rename. The embedded kanban path switches from KanbanCompiler to CardCompiler as part of the framework wide kanban to card migration; XML using mode="kanban" is expected to keep working.

We will re verify this page against the released branch after the launch.

Common problems and fixes

SymptomCause and fix
A many2many column in a list view shows 3 records instead of tagsList cells are served by the list.many2many variant, which prints a plain text count and loads no subview. Set widget="many2many_tags" on the field in the list view.
Removing a line deleted the record (or did not, when you expected deletion)many2many mode always unlinks; deletion semantics belong to one2many mode. Check the field type and widget name; use widget="many2many" on a one2many to get unlink behavior on purpose.
options="{'edit': False}" has no effectThe component overwrites the edit key internally with the record's edition state. Use the write option (19+) or make the subview non editable in its arch.
Users can still create records although create is falseIn many2many mode the Add button follows link, not create; the dialog's Create button is what create removes. Set both: options="{'link': ..., 'create': False}".
no_create or no_quick_create is ignoredThose are many2one options; this widget never reads them. Use the five crud keys (create, delete, link, unlink, write) instead.

Many2many table widget vs the alternatives

WidgetBest forKey difference
many2manyManaging linked records in a full embedded list or kanbanComplete subview with pager and selection dialog; removal unlinks rather than deletes
many2many_tagsCompact tag display and quick pickingColored pills in one line instead of an embedded table
many2many_checkboxesSmall fixed sets picked by tickingRenders every candidate as a checkbox, capped at 100 records
one2manyChild records owned by the parentSame component in ownership mode: rows are created and deleted, not linked
many2many_binaryAttachment collectionsSpecialized for ir.attachment with an upload button

The full table earns its screen space when users work inside the linked records. For compact display or picking only, the tag and checkbox widgets below do the same relation with far less UI.

Frequently asked questions

What is the many2many widget in Odoo?+
The default embedded table for many2many fields: a real list or kanban of the linked records with an Add line that opens a selection dialog. It is the same component as the one2many widget, switched into link and unlink mode.
Can I put widget="many2many" on a one2many field?+
Yes, and Odoo does it in stock_picking_batch. The component checks the widget name as well as the field type, so a one2many rendered this way gets many2many semantics: removing a row unlinks it instead of deleting the record.
How do I stop users adding or removing lines conditionally?+
Use the crud options with domains: options="{'link': [('state', '=', 'draft')], 'unlink': [('state', '=', 'draft')]}". The domain is evaluated against the parent record, no code needed.
Why does the Add dialog not show some records?+
The widget appends a domain that excludes already linked records (their ids), on top of any domain the field carries. Check both before suspecting a data problem.
What changes for this widget in Odoo 20?+
On the development branch the five crud keys (create, delete, link, unlink, write) are read from XML attributes instead of the options dictionary, so existing options strings will need rewriting after the release. The kanban internals also rename to card.

Relational screens your operators can trust

Wrong unlink versus delete semantics silently destroy data, and the Odoo 20 attribute change will touch every custom x2many view you own. We audit relational views, encode the rules as domains, and prepare your XML for the next migration.

Audit my x2many views

How this page was produced

This page was verified by reading the Odoo 19.0 web module source at views/fields/x2many/x2many_field.js and list_x2many_field.js, including useActiveActions, the select and create dialog wiring and the many2many type check, then diffing against the 18.0 branch and the public development branch for the version notes. Behavior was confirmed on a clean Odoo 19 database, including the one2many forcing trick from stock_picking_batch. Corrections via our contact page.