Skip to main content
iVentureTeam

question_page_one2many

The survey question list looks like an ordinary embedded list until you add a section. question_page_one2many is the widget that makes sections behave, and it saves the whole survey behind your back on every edit.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 27, 2026Updated August 27, 20267 min read
Technical namequestion_page_one2many
Field typesone2many, many2many
Viewsform, with an embedded list and an optional kanban
Modulesurvey
Used in core1 occurrence, the Questions tab of the survey form in survey
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0, Odoo 17.0, Odoo 16.0
No-code setupNo. It is a view XML widget and depends on a discriminating boolean field on the child model
Alternativesone2many, many2many, survey_description_page, portal_wizard_user_one2many

What the question list field does

A survey is a flat list of questions that users experience as a structured document: sections with titles, questions grouped under them, an order that matters. Odoo stores both in one relation, with a boolean on each row saying whether it is a page or a question. The widget's job is to make that single list read like two different things.

It does three separate things to achieve that. It swaps in a list renderer that knows about the section flag, so section rows render bold, take their own layout and let the title span the columns a question would fill. It routes creation differently, editing sections inline and opening questions in a dialog. And it forces the parent survey to be saved every time a question is created or edited, so that rules spanning several questions can be enforced right away instead of at the end.

That last piece is why the widget also installs a custom error handler. A failed save in the middle of an inline edit would normally raise Odoo's full-screen error dialog, which is a poor response to a missing question title. The widget converts it into a notification and quietly removes the row that could not be saved.

What this means for your team

Sections are how a long survey stops being intimidating. Grouping twenty questions into four named blocks changes completion rates, and it changes them more than any wording tweak. The widget is what makes those sections cheap enough to use, since adding one is an inline row rather than a form.

The forced save is a trade rather than a free win. It means a survey author cannot build up several questions and then discard them all, because each one is committed as it is created. In exchange, contradictions are caught at the moment they are introduced, which for surveys matters because questions can act as triggers for other questions and a broken chain is hard to debug later.

For anyone reusing the pattern in custom work, the section idea generalizes well: any list where some rows are structure and some are content. Sale order lines already use a comparable arrangement. The renderer this widget installs is tied to survey field names, so a custom version means a small renderer subclass of your own, not just a widget attribute.

Supported options in Odoo 19

The widget declares no supported options, and neither does the x2many descriptor it spreads. What you write in the options dictionary is forwarded to the embedded list as CRUD settings. The real controls are attributes and companion fields, listed below and verified against the Odoo 19.0 source and the core survey form.

OptionTypeWhat it does
is_pagefield on the child modelHardcoded discriminator. Rows where it is true render as sections. The name is written literally in the renderer and cannot be changed from XML.
titlefield on the child modelHardcoded title column. Section rows merge the columns following it, up to the next column the renderer is told to keep.
default_is_pagecontext key on the create buttonSet on a <create> control's context. When true, and the screen is not small, the new row is added inline; otherwise the row opens in a dialog.
editablestring (list attribute)The widget defaults the embedded list to editable at the bottom, unlike a plain one2many which is not editable unless the list says so.(default: bottom)
modestring (field attribute)Which subviews are available. Core uses list,kanban so the questions can also be shown as cards.

The section behavior is hardcoded, not configured. The renderer uses the field name is_page as its discriminator and title as the title column, both written literally in the source. A model that names those fields differently will render as an ordinary list with no sections at all, and nothing will warn you.

Working examples

The core usage, trimmed

<field name="question_and_page_ids" nolabel="1"
       widget="question_page_one2many" mode="list,kanban"
       context="{'default_survey_id': id}">
  <list decoration-bf="is_page">
    <field name="sequence" widget="handle"/>
    <field name="title" widget="survey_description_page"/>
    <field name="question_type"/>
  </list>
</field>

Note the companion widget on the title column: it relies on an open-record helper that this widget publishes to its children.

The add buttons that produce sections

<control>
  <create string="Add a question"/>
  <create string="Add a section"
          context="{'default_is_page': True}"/>
</control>

The default_is_page key in the button's context is what makes the new row editable inline; without it the row opens in a dialog.

Reusing the widget on another model

<field name="line_ids" widget="question_page_one2many">
  <list>
    <field name="is_page" column_invisible="True"/>
    <field name="title"/>
  </list>
</field>

Possible, but the child model must genuinely carry fields named is_page and title, because the renderer names them literally.

Forced parent saves, and why a bad row disappears

The widget is a component subclass plus a descriptor. The subclass swaps ListRenderer for the survey renderer, defaults the list to editable at the bottom, and publishes an openRecord helper into the environment so child field widgets in the rows can request that a row be opened.

The save machinery is the substantial part. It takes the standard create and update helpers, wraps each one so that after the child row is written the parent record is saved as well, and rebuilds the open-record helper on top of those wrappers. If the parent save throws while creating, the widget deletes the offending row from the list before re-throwing a private error type. A handler registered globally in the error-handlers registry catches that type and shows the server's message as a danger notification titled Validation Error, which is what stops a missing title from raising a full error dialog.

Opening an existing row is guarded too. The override saves the parent record first and returns without opening anything if that save fails, on the reasoning that a form you cannot save is not worth opening. It then re-resolves the row from the current list by its database id, so the dialog edits the live record rather than a stale copy.

The renderer holds the section logic. It marks rows using the is_page field, adds a section list class to the table, applies bold and section classes to those rows, and merges columns between the title column and the next column it is told to keep, which in the survey case is the random question count. Its create override is the piece users feel: inline editing is enabled only when the create button's context sets the section default and the screen is not small, so on a phone every addition opens a dialog.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. Same behavior on the development branch; props and service lookup are modernized.
Odoo 19.0VerifiedVerified against the shipped source. Same behavior as Odoo 18.
Odoo 18.0VerifiedIdentical behavior, including the forced parent save and the Validation Error notification.
Odoo 17.0VerifiedThis is where the forced parent save, the failed-row cleanup and the custom error handler were introduced.
Odoo 16.0Partial / changedSection rendering and the open-record helper only. No forced parent save, no custom error handling, and the component was registered directly rather than as a descriptor.

Upgrade note. No XML change is needed anywhere between Odoo 16 and Odoo 19, but the behavior is not the same. Odoo 16 has only the renderer swap and the open-record helper; the forced parent save, the row cleanup and the Validation Error notification all arrive in Odoo 17. If a custom module patched this widget on Odoo 16, expect the patch to be built on a much smaller class than the one it will meet on 17 and later.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. The development branch is unstable and the notes below may still change; we re-verify the page after release.

Behavior is unchanged. The section rendering, the forced parent save, the row cleanup and the Validation Error notification are all present on the development branch, and core still uses the widget on the survey form.

The internals move. The props are migrated to the new Owl schema syntax, and because the base x2many props are not yet exported as a shared schema the widget inlines them, with a source comment saying exactly that. The notification service is also resolved through a hook instead of the environment. Both changes matter only to JavaScript that patches this widget.

Common problems and fixes

SymptomCause and fix
Sections do not render differently from questionsThe child model has no field named is_page, which the renderer uses literally. Add that field name to the child model, or subclass the renderer with your own discriminator.
Adding a section opens a dialog instead of editing inlineThe create control's context does not set default_is_page, or the screen is treated as small. Add context="{'default_is_page': True}" to that create control. On a phone the dialog is intentional.
A new question disappeared after I filled it inThe parent save failed, so the widget removed the row and raised a Validation Error notification. Read the notification, fix the cause on the survey or another question, then add the question again.
Clicking a question does nothingThe parent record could not be saved, and the widget refuses to open a dialog in that state. Resolve the pending validation error on the survey form, then reopen the question.
The survey saves itself while I am still editingIntended. Every question create or update triggers a save of the parent record. Build questions with that in mind; there is no option to disable it.
Section titles do not span the columnsThe column layout does not match what the renderer expects, most often because the title column is not present. Include the title field in the embedded list, and keep the count column the renderer preserves.
The pencil icon on section rows is missingThat icon comes from the companion title widget, not from this one. Set widget="survey_description_page" on the title column, as the core view does.

Question list field vs the alternatives

WidgetBest forKey difference
question_page_one2manyOne relation that mixes structural section rows with content rowsSection-aware renderer plus a forced save of the parent record on every child edit
one2manyAn ordinary list of child recordsNo section rendering and no parent save; rows are committed with the form
many2manyLinking existing records rather than owning themAdds and removes links instead of creating and deleting rows
survey_description_pageThe title cell inside this listA cell widget, not a list widget; it renders the open-section button
portal_wizard_user_one2manyWizard lists with slow row buttonsSwaps the controller for click protection rather than the renderer for sections

Use a plain one2many whenever the rows are all the same kind of thing. This widget earns its complexity only when a single relation carries both structure and content, and when a change to one row must be validated against the others immediately. If you want the section pattern without the forced saves, the renderer is the part worth copying.

Frequently asked questions

Why does the survey save itself when I add a question?+
The widget wraps the create and update helpers so that the parent survey is saved after each child write. It exists so that validation rules spanning several questions, including question triggers, are enforced at the moment they are broken rather than at the end.
Why did my new question vanish?+
The parent save failed. The widget deletes the row that could not be saved and raises a private error type, which a registered handler turns into a red Validation Error notification instead of a full error dialog.
Why does adding a section edit inline while adding a question opens a dialog?+
The renderer only enables inline editing when the create button's context sets default_is_page and the screen is not small. Sections carry that key in core; questions do not.
Can I use question_page_one2many on my own model?+
Yes, but the child model must have fields literally named is_page and title, because the renderer references them by name. Anything else renders as a plain list with no sections.
What options does it support?+
None of its own. Like every x2many widget it forwards the whole options dictionary to the embedded list as CRUD settings. The controls that matter are the list attributes and the create button contexts.

Building a document your users edit as a list?

Sectioned lists, inline editing and validation that fires at the right moment are the difference between an editor people enjoy and one they route around. We design and build that kind of screen on Odoo 16 through 19, renderer included.

Book a free consultation

How this page was produced

The renderer swap, the forced parent save, the error handler and the open-record guard were read from question_page_one2many_field.js and question_page_list_renderer.js on the Odoo 19.0 branch, with the usage taken from survey/views/survey_survey_views.xml. The Odoo 16 comparison and the version table come from the same files on the 16.0, 17.0 and 18.0 branches, and the Odoo 20 notes from the public development branch. Spotted an error? Tell us and we will correct the page.