Skip to content

Form Views

Form views display and edit single records.

{
"data_type": "UiView",
"identifier": "contact_form_view",
"type": "Form",
"model": "Contact",
"arch": [
{
"type": "row",
"content": [
{
"type": "column",
"span": 6,
"content": [
{
"type": "field",
"name": "name",
"properties": {
"widget": "TextInput",
"size": "lg"
}
}
]
}
]
}
]
}

Forms use a 12-column grid system:

{
"type": "row",
"content": [
{"type": "column", "span": 6, "content": [...]},
{"type": "column", "span": 6, "content": [...]}
]
}
{
"type": "column",
"span": 6,
"content": [
{
"type": "row",
"content": [
{"type": "column", "span": 6, "content": [...]},
{"type": "column", "span": 6, "content": [...]}
]
}
]
}

Rows use a responsive gutter (spacing between columns) that defaults to { base: 16, md: 100 }. You can override this per-row using the properties.gutter property:

{
"type": "row",
"properties": {
"gutter": 16
},
"content": [
{"type": "column", "span": 4, "content": [...]},
{"type": "column", "span": 4, "content": [...]},
{"type": "column", "span": 4, "content": [...]}
]
}
Gutter ValueUse Case
16Tight spacing for related fields (e.g., City/State/Zip)
{ base: 16, md: 100 }Default - responsive spacing
Custom numberFixed pixel spacing

Display status at the top:

{
"type": "statusbar",
"name": "status",
"properties": {
"widget": "Badge",
"variant": "dot",
"colors": {
"draft": "gray",
"confirmed": "blue",
"done": "green",
"cancelled": "red"
}
}
}

Trigger model methods:

{
"type": "actionButton",
"properties": {
"label": "Confirm",
"icon": "Check",
"variant": "primary",
"method": "confirm_order",
"visible": "Q(status__eq='draft')"
}
}
PropertyDescription
labelButton text
iconLucide icon name
variantprimary, outline, subtle
methodModel method to call
contextCustom context passed to method
auto_refetchReload after action
instancePass instance to method

Action buttons automatically pass context to backend methods. Access via self._context:

{
"type": "actionButton",
"properties": {
"label": "Send Email",
"method": "action_send_email",
"context": {
"template": "welcome_email",
"send_copy": true
}
}
}
class Contact(Model):
async def action_send_email(self):
# Auto-provided context
active_model = self._ctx.get('active_model') # 'Contact'
active_ids = self._ctx.get('active_ids') # Selected record IDs
# Custom context from button
template = self._ctx.get('template') # 'welcome_email'
send_copy = self._ctx.get('send_copy') # True
for record_id in active_ids:
record = await Contact.get(record_id)
await record.send_email(template=template)
return {'type': 'close', 'reload': True}

Auto-provided context keys:

KeyDescription
active_modelModel name
active_idsList of selected record IDs

Show related record counts:

{
"type": "linkButton",
"name": "order_count",
"properties": {
"label": "Orders",
"icon": "ShoppingCart",
"action": "orders_action",
"filter": "customer"
}
}

Organize content in tabs:

{
"type": "tab",
"title": "Contact Info",
"properties": { "icon": "User" },
"content": [
{"type": "field", "name": "email", "properties": {"widget": "TextInput"}},
{"type": "field", "name": "phone", "properties": {"widget": "TextInput"}}
]
}

Collapsible sections:

{
"type": "accordion",
"title": "Additional Details",
"defaultOpen": false,
"properties": { "icon": "Info" },
"content": [
{"type": "field", "name": "notes", "properties": {"widget": "TextArea"}}
]
}

Both accordions and tabs support an optional properties.icon property using Lucide icon names. If no icon is specified, LayoutList is used as the default.

PropertyDescription
properties.iconLucide icon name (e.g., User, ShoppingCart, Settings)
defaultOpen(Accordion only) Whether to start expanded (default: true)

Page tabs provide a full-width tab navigation bar rendered in the header area (beneath the secondary header). This is ideal for settings pages or forms with multiple sections that need prominent navigation. Tabs automatically collapse into a “More” dropdown when they overflow the available width.

{
"type": "pageTabs",
"content": [
{
"type": "tab",
"name": "general",
"title": "General",
"properties": { "icon": "DatabaseZap" },
"content": [
{
"type": "paper",
"title": "System Identity",
"content": [
{
"type": "row",
"content": [
{
"type": "column",
"span": 6,
"content": [
{ "type": "field", "name": "system_name" }
]
}
]
}
]
}
]
},
{
"type": "tab",
"name": "security",
"title": "Security",
"properties": { "icon": "Shield" },
"content": [...]
}
]
}
  • Header rendering: Tabs appear in the header area (full-width, not inside form Paper)
  • Pill-shaped styling: Active tab has a light background with pill-shaped borders
  • Overflow handling: Tabs that don’t fit collapse into a “More” dropdown automatically
  • Icon support: Each tab can have an icon via properties.icon (Lucide icon names)
  • Paper cards: Each tab’s content is rendered as separate Paper cards
PropertyDescription
nameUnique identifier for the tab
titleDisplay text for the tab
properties.iconLucide icon name (e.g., Settings, Shield, User)
contentArray of layout elements (paper, row, field, etc.)

Other modules can add tabs to an existing pageTabs using view inheritance:

{
"data_type": "UiView",
"name": "configuration_form_view_mymodule",
"identifier": "configuration_form_view_mymodule",
"inherited_view": "configuration_form_view",
"type": "Form",
"model": "Configuration",
"arch": {
"operations": [
{
"action": "add",
"target": { "type": "pageTabs" },
"position": "inside",
"value": {
"type": "tab",
"name": "mymodule",
"title": "My Module",
"properties": { "icon": "Settings" },
"content": [
{
"type": "paper",
"title": "My Settings",
"content": [
{ "type": "field", "name": "my_setting" }
]
}
]
}
}
]
}
}
Use CaseRecommended
Settings/Configuration formspageTabs - full-width header navigation
Multi-section forms with many tabspageTabs - handles overflow gracefully
Related data within a formRegular tabs - inline with form content
Quick toggles between few sectionsRegular tabs - simpler, inline
{
"type": "field",
"name": "email",
"properties": {
"widget": "TextInput",
"label": "Email Address",
"placeholder": "Enter email",
"required": true
}
}
PropertyDescription
widgetWidget type (TextInput, DataCombo, etc.)
labelField label (overrides model description)
placeholderInput placeholder
nolabelHide the label
sizeSize: xs, sm, md, lg, xl
visibleBoolean or Q-expression for conditional visibility
readonlyBoolean or Q-expression for read-only
requiredBoolean or Q-expression for required
groupsList of group identifiers for access control
filterQ-expression to filter related records (DataCombo/MultiCombo)
contextDefault values for new records (see Field-Level Context)

Display related records:

{
"type": "field",
"name": "order_lines",
"properties": {
"widget": "List",
"create": true,
"delete": true,
"view": "order_line_list_view",
"context": {
"form_identifier": "order_line_form_view",
"default_qty": 1
}
}
}
PropertyDescription
viewList view identifier
createAllow creating records
deleteAllow deleting records
editabletrue for inline editing, "modal" for modal editing
insertPositionWhere new items are added: "bottom" (default) or "top"
contextDefault values and view references

The context object supports:

  • default_<field>: Pre-populate field when adding new rows
  • form_identifier: Form view for modal editing

See Field-Level Context for more details.

All layout elements (row, column, accordion, tabs, tab, fieldset, paper, group), fields, and action buttons support behavioral properties inside the properties object:

PropertyTypeDescription
visibleBoolean/StringIf false, element is hidden. Can be a Q-expression for conditional visibility. Default: true
readonlyBoolean/StringIf true, field(s) are read-only. Can be a Q-expression. Propagates to children.
requiredBoolean/StringIf true, field is required. Can be a Q-expression for conditional requirement.
groupsArrayList of group identifiers. Element is visible only if user belongs to at least one group.

Note: All field configuration goes in properties. Structural attributes (type, name, span, content, title) stay at the top level.

Show/hide elements based on conditions:

{
"type": "field",
"name": "company_name",
"properties": {
"widget": "TextInput",
"visible": "Q(type__eq='company')"
}
}

Hide an element unconditionally:

{
"type": "field",
"name": "internal_id",
"properties": {
"widget": "TextInput",
"visible": false
}
}

Make a field read-only based on conditions:

{
"type": "field",
"name": "price",
"properties": {
"widget": "NumberInput",
"readonly": "Q(status__neq='draft')"
}
}

Make a field required based on conditions. The Q-expression is evaluated dynamically whenever form values change, so validation adapts in real-time:

{
"type": "field",
"name": "shipping_address",
"properties": {
"widget": "DataCombo",
"required": "Q(requires_shipping__eq=true)"
}
}

When requires_shipping becomes true, the field shows the required asterisk and form submission validates it. When requires_shipping is false, the field is optional.

Q expressions use the format Q(field__operator=value):

"Q(status__eq='draft')"
"Q(is_customer=True)"
"Q(amount__gt=1000)"
"Q(stage__is_done__eq=false)"
"Q(type__eq='company') & Q(active__eq=True)"
"Q(status__in=['draft', 'sent'])"
"(Q(a__eq=1) | Q(b__eq=2)) & Q(c__eq=3)"

Operators:

OperatorDescriptionExample
eqEquals (default if omitted)Q(state__eq='Draft') or Q(state='Draft')
neqNot equalsQ(code__neq='en')
gtGreater thanQ(amount__gt=100)
gteGreater than or equalQ(amount__gte=100)
ltLess thanQ(amount__lt=100)
lteLess than or equalQ(amount__lte=100)
containsString contains (case-sensitive)Q(name__contains='test')
icontainsString contains (case-insensitive)Q(name__icontains='test')
ncontainsString does not containQ(name__ncontains='draft')
nicontainsString does not contain (case-insensitive)Q(name__nicontains='draft')
isnullIs null or empty arrayQ(user__isnull=True)
isnotnullIs not null and not emptyQ(user__isnotnull=True)
inValue in listQ(state__in=['Draft','Pending'])
ninValue not in listQ(state__nin=['Done','Cancelled'])
startswithString starts withQ(name__startswith='INV')
endswithString ends withQ(name__endswith='.pdf')

Value types:

  • Booleans: True, False, true, false
  • Numbers: 100, 45.67
  • Strings: 'value' or "value"
  • Arrays (for in/nin): ['Draft', 'Pending']

Shorthand equality: Q(is_customer=True) is equivalent to Q(is_customer__eq=True)

Nested field access (deep nesting supported):

  • Access related fields: Q(stage__is_done__eq=true) accesses record.stage.is_done
  • Deep nesting is fully supported: Q(contact__company__currency__code__eq='USD')
  • The system automatically extracts and prefetches all intermediate relation paths
  • Example: Q(order__partner__country__code__in=['US','CA']) auto-prefetches order, order__partner, order__partner__country

Logical operators:

  • AND: Q(a__eq=1) & Q(b__eq=2)
  • OR: Q(a__eq=1) | Q(b__eq=2)
  • Precedence: & binds tighter than |
  • Grouping: (Q(a__eq=1) | Q(b__eq=2)) & Q(c__eq=3)

Q expressions in visible, readonly, and required properties are re-evaluated whenever form data changes. For conditions that need to update reactively when a field changes, use flat field names that exist directly on the form.

Recommended (reactive):

"visible": "Q(track_inventory__eq=true)"

Not recommended for reactive use:

"visible": "Q(product__track_inventory__eq=true)"

Why? Nested traversal (e.g., product__track_inventory) accesses cached nested objects that are snapshots from when the record was loaded. When you edit a related field like track_inventory, the flat field updates but the nested product object doesn’t refresh. This means:

  • Initial load: Traversal works correctly
  • After editing: Nested object has stale data, condition may not update

Best practices:

ScenarioApproach
Check a field on the current formUse flat field name: Q(status__eq='draft')
Check a related field (has related_field attribute)Use the flat field name: Q(track_inventory__eq=true)
Check a truly nested value (display only)Traversal works for initial state, won’t be reactive
Need reactive behavior for nested computed fieldCreate a related field on your model to expose it

Restrict to specific groups:

{
"type": "field",
"name": "internal_notes",
"properties": {
"widget": "TextArea",
"groups": ["sales_manager", "admin"]
}
}
{
"data_type": "UiView",
"identifier": "order_form_view",
"type": "Form",
"model": "Order",
"arch": [
{
"type": "statusbar",
"name": "status",
"properties": {
"widget": "Badge",
"variant": "dot",
"colors": {
"draft": "gray",
"confirmed": "blue",
"shipped": "orange",
"delivered": "green",
"cancelled": "red"
}
}
},
{
"type": "actionButton",
"properties": {
"label": "Confirm",
"icon": "Check",
"variant": "primary",
"method": "confirm_order",
"visible": "Q(status__eq='draft')"
}
},
{
"type": "actionButton",
"properties": {
"label": "Ship",
"icon": "Truck",
"variant": "primary",
"method": "ship_order",
"visible": "Q(status__eq='confirmed')"
}
},
{
"type": "actionButton",
"properties": {
"label": "Cancel",
"icon": "X",
"variant": "outline",
"method": "cancel_order",
"visible": "Q(status__in=['draft', 'confirmed'])"
}
},
{
"type": "row",
"content": [
{
"type": "column",
"span": 8,
"content": [
{
"type": "field",
"name": "name",
"properties": {"widget": "TextInput", "size": "lg", "nolabel": true}
}
]
},
{
"type": "column",
"span": 4,
"content": [
{
"type": "field",
"name": "order_date",
"properties": {"widget": "DatePickerInput"}
}
]
}
]
},
{
"type": "row",
"content": [
{
"type": "column",
"span": 6,
"content": [
{"type": "field", "name": "customer", "properties": {"widget": "DataCombo"}},
{"type": "field", "name": "shipping_address", "properties": {"widget": "DataCombo"}}
]
},
{
"type": "column",
"span": 6,
"content": [
{"type": "field", "name": "payment_terms", "properties": {"widget": "DataCombo"}},
{"type": "field", "name": "salesperson", "properties": {"widget": "DataCombo"}}
]
}
]
},
{
"type": "tab",
"title": "Order Lines",
"content": [
{
"type": "field",
"name": "lines",
"properties": {
"widget": "List",
"create": true,
"delete": true,
"view": "order_line_list_view"
}
}
]
},
{
"type": "tab",
"title": "Notes",
"content": [
{"type": "field", "name": "notes", "properties": {"widget": "RichTextEditor"}}
]
},
{
"type": "row",
"content": [
{"type": "column", "span": 6, "content": []},
{
"type": "column",
"span": 6,
"content": [
{"type": "field", "name": "subtotal", "properties": {"widget": "NumberInput", "readonly": true}},
{"type": "field", "name": "tax_amount", "properties": {"widget": "NumberInput", "readonly": true}},
{"type": "field", "name": "total", "properties": {"widget": "NumberInput", "size": "lg", "readonly": true}}
]
}
]
}
]
}