create-page

CREATE PAGE syntax reference — parameters, variables, layouts, and the full widget vocabulary. Use before writing any CREATE PAGE statement, or when a widget's property spelling needs checking.

SKILL.md

GitHub
<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->

Find it fast

sed -n '<from>,<to>p' SKILL.md one section rather than reading the file; ./mxcli syntax <topic> answers how do I write this for a fraction of either.

lines section
28-43 Reference files
44-46 Overview
47-196 Syntax
197-267 Basic Examples
268-289 Modifying Existing Pages
290-393 Conditional Visibility and Editability
394-558 Known Limitations
559-567 Tips
568-594 Related Commands
595-619 PLUGGABLEWIDGET Escape Hatch
620-623 See Also
<!-- /skill-index -->

CREATE PAGE - MDL Syntax Guide

Reference files

SKILL.md covers page structure, the syntax, and what does not work. The bulk is next door:

  • reference/widgets.md — the widget catalogue: every supported widget with its properties and the exact MDL spelling. Look a widget up here before writing it; guessing a property name is the most common way a page fails to build.
  • reference/examples.md — complete pages, end to end, to copy and adapt rather than assemble from parts.

For the widgets this project actually has — including any marketplace or custom ones — read the generated widgets skill instead (.ai-context/skills/widgets/SKILL.md).

Overview

Guide for writing CREATE PAGE statements in Mendix Definition Language (MDL).

Syntax

create [or replace] page Module.PageName
(
  [params: ( $ParamName: Module.EntityType | PrimitiveType, ... ),]
  [variables: ( $varName: DataType = <default expression>, ... ),]   -- bare: `= true`, not `= 'true'`
  title: 'Page Title',
  layout: Module.LayoutName,
  [url: 'page-url',]
  [folder: 'FolderPath',]
  [PopupWidth: 800, PopupHeight: 480, PopupResizable: true,]
  [Class: 'css-class', Style: 'css: rule']
)
{
  -- Widget definitions using explicit properties
}

Pop-up dimensions (PopupWidth / PopupHeight / PopupResizable) apply when the page is opened in a pop-up. They are optional — omitting them uses the Mendix defaults (600 × 600, not resizable). Unlike the other header keywords, these property names are case-sensitive and must be written exactly as shown. They can also be changed later with alter page … { set (PopupWidth: …); } (see the alter-page skill).

Page CSS class / style (Class / Style) set the page's Appearance — a CSS class and inline style applied to the whole page (e.g. Class: 'container-fluid bg-light'). Both are optional and can be changed later with alter page … { set (Class: '…'); }.

Page Variables: Local variables at the page level for use in expressions (e.g., column visibility).

  • DataType: boolean, string, integer, decimal, datetime
  • Default value: Mendix expression in single quotes
  • Referenced in expressions as $varName
  • Use for DataGrid2 column visible: (which hides/shows entire column, NOT per-row)
  • An input binds to one directly: checkbox cbShowAll (Label: 'Show all', Attribute: $ShowAll) — no data view needed. $name must be declared in the page's Variables:

Key Syntax Elements

Element Syntax Example
Properties (key: value, ...) (title: 'Edit', layout: Atlas_Core.Atlas_Default)
Widget name Required after type textbox txtName (...)
Attribute binding attribute: AttrName textbox txt (label: 'Name', attribute: Name)
Attribute over an association attribute: Assoc/Attr (bare association name, multi-hop OK) textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)
Password field Password: true textbox tbPw (attribute: Secret, Password: true)
Submit while typing SubmitBehaviour: WhileEditing (+ SubmitOnInputDelay: <ms>) on textbox / textarea textbox tbSearch (attribute: Query, SubmitBehaviour: WhileEditing, SubmitOnInputDelay: 500) — default OnEndEditing / 300
Layout grid width Width: FullWidth | FixedWidth on layoutgrid layoutgrid lg (Width: FixedWidth) { ... } — default FullWidth
Widget validation Validation: '<expr>' + ValidationMessage: '<text>' Validation: 'length(toString($value)) > 0' — quoted, not [bracketed]
Variable binding datasource: $Var dataview dv (datasource: $Product) { ... }
Action binding action: type actionbutton btn (caption: 'Save', action: save changes)
Database source datasource: database entity datagrid dg (datasource: database Module.Entity)
Database source over an association datasource: database from $Ctx/Assoc/Entity — list view; on Mendix 9 also a pluggable widget (repeater, gallery, grid), whose where must then be simple rows (Attr op value joined by and) repeater r (datasource: database from $Form/M.Item_Form/M.Item sort by Seq asc)
Selection binding datasource: selection widget dataview dv (datasource: selection galleryList)
CSS class class: 'classes' container c (class: 'card mx-spacing-top-large')
Inline style style: 'css' container c (style: 'padding: 16px;')
Design properties designproperties: (...) container c (designproperties: ('Spacing top': 'Large', 'full width': on))

FOLDER Option

Place pages in folders for better organization:

mdl 1;
create page MyModule.CustomerEdit folder 'Customers'
(
  title: 'Edit Customer',
  layout: Atlas_Core.PopupLayout
)
{
  -- widgets
};

-- Nested folders (created automatically if they don't exist)
create page MyModule.OrderDetail folder 'Orders/Details'
(
  title: 'Order Details',
  layout: Atlas_Core.Atlas_Default
)
{
  -- widgets
};

Styling: Class, Style, and DesignProperties

Three styling mechanisms can be applied to any widget:

CSS Class — Atlas UI utility classes or custom CSS classes:

container c (class: 'card mx-spacing-top-large') { ... }
actionbutton btn (caption: 'Save', class: 'btn-lg')

Inline Style — One-off CSS styles (use sparingly):

container c (style: 'background-color: #f8f9fa; padding: 16px;') { ... }

Warning: Do NOT use style directly on DYNAMICTEXT widgets — it crashes MxBuild with a NullReferenceException. Wrap the DYNAMICTEXT in a styled CONTAINER instead.

Design Properties — Atlas UI structured properties (spacing, colors, toggles):

-- Option property: 'Key': 'Value'
container c (designproperties: ('Spacing top': 'Large', 'Background color': 'Brand Primary')) { ... }

-- Toggle property: 'Key': ON (enabled) or OFF (disabled/omitted)
container c (designproperties: ('Full width': on)) { ... }

-- Multiple types combined
actionbutton btn (caption: 'Save', designproperties: ('Size': 'Large', 'Full width': on))

Dynamic Classes — a Mendix expression evaluated at runtime that returns a class list (applied on top of the static class). Write the expression as-is — no outer quotes, no doubled ones — and root attributes in $currentObject. A quoted value is a Mendix string: dynamicclasses: 'is-featured' is the class is-featured.

dynamictext ovChip (
  content: 'chip',
  class: 'ss-chip',
  dynamicclasses: if $currentObject/VesselClass = Mod.BoatClass.Astute then 'ss-chip--astute' else ''
)

Not in brackets: dynamicclasses: [ … ] (and a column's DynamicCellClass: [ … ]) parses as a list, which no writer reads — check reports it as MDL-WIDGET32. And not the old quoted spelling 'if … then ''a'' else ''''', which would now store the expression's text as a class name — under mdl 1; check refuses it as MDL-WIDGET33; without the header it keeps its old meaning and warns MDL-V1-QUOTEDEXPR, and mxcli fmt --upgrade writes it bare.

All can be combined on a single widget:

container ctnHero (
  class: 'card',
  style: 'border-left: 4px solid #264AE5;',
  dynamicclasses: if $currentObject/Featured then 'is-featured' else '',
  designproperties: ('Spacing top': 'Large', 'Full width': on)
) {
  dynamictext txtTitle (content: 'Styled Container', rendermode: H3)
}

mxcli check warns (MDL-WIDGET07) when a built-in widget carries an unrecognized property (a typo, or a property mxcli doesn't persist) — it would otherwise be silently dropped on write. It is a warning, not an error, so the check still passes; fix the spelling or remove the property.

Basic Examples

Simple Page with Title

mdl 1;
create page MyModule.HomePage
(
  title: 'Home Page',
  layout: Atlas_Core.Atlas_Default
)
{
  dynamictext welcomeText (content: 'Welcome to My App', rendermode: H1)
};

Page with Multiple Widgets

mdl 1;
create page MyModule.CustomerPage
(
  title: 'Customer Details',
  layout: Atlas_Core.Atlas_Default
)
{
  layoutgrid mainGrid {
    row {
      column (desktopwidth: 12) {
        dynamictext heading (content: 'Customer Information', rendermode: H2)
      }
    }
    row {
      column (desktopwidth: 6) {
        actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary)
      }
      column (desktopwidth: 6) {
        actionbutton btnCancel (caption: 'Cancel', action: cancel changes)
      }
    }
  }
};

Layout Placeholders (multiple content areas)

By default all top-level widgets bind to the layout's Main placeholder. When a layout has more than one placeholder (e.g. Main + a sidebar/topbar), use a placeholder <Name> { … } block to assign widgets to a specific placeholder. Bare widgets still bind to Main.

mdl 1;
-- Atlas_Core.Atlas_SideBar has two placeholders: Main and Topbar
create page MyModule.Dashboard (title: 'Dashboard', layout: Atlas_Core.Atlas_SideBar)
{
  placeholder Main {
    dynamictext lblMain (content: 'Main content area')
  }
  placeholder Topbar {
    dynamictext lblTop (content: 'Top bar content')
  }
};

Notes:

  • The placeholder name must match a placeholder defined in the layout (e.g. Main, Right, Topbar, Content — depends on the layout). An unknown name fails mx check.
  • Keyword-like names (Right, Left, Content) are accepted.
  • describe page emits placeholder blocks for multi-placeholder pages so they round-trip.

Modifying Existing Pages

To make targeted changes to an existing page (change a label, add a field, remove a widget), use alter page instead of create or replace page. ALTER PAGE modifies the widget tree in-place, preserving properties that MDL doesn't model.

mdl 1;
-- Change a button caption and add a field
alter page Module.Customer_Edit {
  set (caption: 'Save & Close') on btnSave;
  insert after txtEmail {
    textbox txtPhone (label: 'Phone', attribute: Phone)
  }
};

Use create or modify page on an existing page only if your MDL scripts created it and nobody has edited it in Studio Pro since. A Studio Pro-authored page is changed with alter page, even for a large change: re-creating it from describe output has dropped translations. See choose-edit-mode.

See the dedicated skill file: ALTER PAGE/SNIPPET

Conditional Visibility and Editability

Any widget (including CONTAINER) can have conditional visibility. Input widgets can also have conditional editability. The condition is a Mendix client expression, written bare like every expression in MDL and stored exactly as written — so name the context object's attributes as $currentObject/Attr:

-- Conditionally visible widget (boolean attribute)
textbox txtName (label: 'Name', attribute: Name, visible: $currentObject/IsActive)

-- Conditionally visible container
container ctnDetails (visible: $currentObject/Name != '') { dynamictext t (content: '...') }

-- Conditionally editable input (boolean)
textbox txtStatus (label: 'Status', attribute: status, editable: $currentObject/CanEdit)

-- Enum comparison: use the QUALIFIED enum value.
textbox txtNotes (label: 'Notes', attribute: Notes,
  visible: $currentObject/Status = MES.EquipmentStatus.Running)

-- Combined
textbox txtEmail (label: 'Email', attribute: Email,
  visible: $currentObject/ShowEmail,
  editable: $currentObject/CanEdit)

-- Static values still work — a plain value is not a condition
textbox txtReadOnly (label: 'Read Only', attribute: Name, editable: Never)
textbox txtHidden (label: 'Hidden', attribute: Name, visible: false)

-- Function calls work, including functions whose name is also an MDL keyword
-- (trim, length, find).
dynamictext tTrim (content: 'x', visible: trim($currentObject/Slug) != '')
textbox txtSlug (label: 'Slug', attribute: Slug, editable: length($currentObject/Slug) > 0)

The older bracketed form, visible: [IsActive], still parses: it roots a bare attribute in $currentObject for you, and warns MDL-DEPR081. mxcli fmt --upgrade rewrites it to the expression it stores (visible: $currentObject/IsActive). A constant condition (editable: [false], stored as the condition false, not as Never) has no bare spelling and keeps its brackets.

Visible based on an attribute value (Studio Pro's "Visible: based on attribute value") — list the Boolean/enumeration values that SHOW the widget; empty is "(empty)". Only an attribute of the enclosing data container's own entity:

container cntRunning (visible: Status in (Running, empty)) { ... }
textbox txtPassword (label: 'Password', attribute: Password, visible: IsLocalUser in (true))

Visible for module roles (Studio Pro's role-based visibility) is a separate setting — a second visible: line that combines with either condition above. It works on widgets, tab pages and navigation-list items; check --references refuses a role that does not exist:

mdl 1;
create page MyModule.Request_Review (title: 'Review', layout: Atlas_Core.Atlas_Default) {
  container cntAdmin (visible: roles (MyModule.Administrator)) {
    dynamictext txtAdmin (content: 'Administrators only')
  }
  actionbutton btnClose (caption: 'Close', action: close page,
    visible: false, visible: roles (MyModule.Manager))
};

visible:/editable: is a Mendix expression, not XPath — a different function set from a datasource where [ ... ] clause:

visible: / editable: (client expression) where [ … ] (XPath)
String tests trim(), length(), toUpperCase(), find(), contains() contains(), starts-with(), ends-with(), string-length()
length() character count number of elements in a list
Emptiness $currentObject/X != '' / != empty [X = empty] or [X = NULL] — a keyword, never empty(…)
Aggregates not available count()/avg()/min()/max()/sum() are Java-API-only

mxcli's grammar accepts any function name in both and lets MxBuild adjudicate, so a wrong-context call surfaces as CE0117 "Error(s) in expression" at build rather than as a parse error. See the Mendix reference guide: XPath constraint functions, XPath keywords.

An unparseable conditional is an error, not a silent drop. If the expression in visible: / editable: can't be parsed, the property has nowhere to go and would vanish on write — leaving the widget unconditionally visible/editable, which looks identical to a specificity bug in the running app. mxcli check reports this as MDL-WIDGET19 and fails the command instead. Until v0.16.x, trim(…) and length(…) hit exactly this path and disappeared without a word (issue #852).

Attributes are not rooted for you in the bare form — the expression is stored as written, so a bare Name is CE0117. Write $currentObject/Name (or $Param/…). Only the deprecated bracketed form roots a bare attribute.

Enum comparison differs by context:

  • Widget visibility/editability expression (per-object): qualified enum value — $currentObject/Status = MES.EquipmentStatus.Running.
  • XPath datasource constraint (where […]): the string key works — where [Status = 'Running'] (see xpath-constraints).
  • Microflow expression: qualified value — $obj/Status = MES.EquipmentStatus.Running (see write-microflows).

Widget-level visibility does not apply to DataGrid2 column visible: (next section), which hides/shows the whole column and must use page variables.

Known Limitations

The following features are NOT implemented in mxcli and require manual configuration in Studio Pro:

Feature Workaround
Nested dataviews filtering by parent Use microflow datasource or configure in Studio Pro
Complex conditional visibility Configure visibility rules in Studio Pro
Widget-level security Configure access rules in Studio Pro

Runtime Pitfalls

An empty CONTAINER is valid. It passes mx check, builds, and renders: measured on Mendix 11.13.0 (React client) under run --local, both a bare container c1 and a styled spacer container spacer1 (Style: 'height: 6px;') rendered with the widgets after them present and no console errors (ako/mxcli#969). An older note here said it crashed with "Did not expect an argument to be undefined"; that was never measured. mxcli lint still reports one as MPR006 at info level, since an empty container is usually a leftover.

content: '' (empty string) fails MxBuild. An empty Content on DYNAMICTEXT causes a misleading error: "Place holder index 1 is greater than 0, the number of parameter(s)." Use a single space instead:

-- Wrong: MxBuild error
DYNAMICTEXT spacer (Content: '')

-- Correct: use a space
DYNAMICTEXT spacer (Content: ' ')

IMAGE needs a source

An image widget shows an entry from an image collection by default, and the entry is named as three parts — Module.Collection.ImageName:

image imgLogo (Image: 'MyFirstModule.Images._1', Width: 48, Height: 48)

list image collections lists the collections; describe image collection MyFirstModule.Images lists the images inside one.

An image with that (default) source and no Image: builds into a model mxbuild refuses — "No image selected." — so mxcli check reports it as MDL-WIDGET22 before you spend a build on it. A name that does not resolve is reported by mxcli check --references, which is cheaper than mxbuild's CE1613 "The selected image … no longer exists."

The two other sources take no collection entry:

image imgRemote (ImageType: imageUrl, ImageUrl: 'https://example.com/logo.svg')
image imgIcon   (ImageType: icon)

An uploaded image — an object of an entity that extends System.Image — is shown by binding the widget to the data view's object. It is not an ImageType (ImageType: dynamicImage is not a source; check warns), and not dynamicimage, which is CE0582 in a React app:

dataview dvPhoto (DataSource: $Photo) {
  image imgPhoto (Image: $currentObject, DefaultImage: 'Atlas_Web_Content.Content.user')
}

DefaultImage: is shown while the object holds no image. It is only valid with $currentObject — Studio Pro hides it otherwise, and a stored value is CE0463.

A button's icon is one of three elements

Mendix stores three different icon elements, and the keyword picks which:

actionbutton btnEdit  (Caption: 'Edit',  Action: nothing, Icon: 'Atlas_Core.Atlas_Filled.pencil')
actionbutton btnLogo  (Caption: 'Logo',  Action: nothing, Icon: image MyFirstModule.Images.logo)
actionbutton btnHome  (Caption: 'Home',  Action: nothing, Icon: glyph 57377)
form element holds
bare name Forms$IconCollectionIcon a name in an icon collection
image <name> Forms$ImageIcon a name in an image collection
glyph <code> Forms$GlyphIcon a font character code, no name

The first two are spelled identically and point into different documents, so the image keyword is the only thing separating them. Write an image reference without it and mxcli stores a custom-icon reference, which fails the build with CE1613 "The selected custom icon … no longer exists." mxcli check -p app.mpr --references resolves each kind against its own collection and names the remedy when the kind is wrong, which is cheaper than a build.

Any icon collection works, third-party ones included — list icon collections lists them and describe icon collection Atlas_Core.Atlas_Filled lists the names (they are non-obvious: it is add, not plus).

A glyph has a code and no name. The codes are sparse, and an undefined one fails only at mxbuild --target=deploy — with "An exception occurred while exporting page '<name>'", naming the page and never the icon — so mxcli check refuses it as MDL078, an error (exec writes nothing), naming the defined glyphs either side of the code. Browse them with list glyphs.

describe page emits all three forms, so describe → exec round-trips a button's icon whichever kind it is.

Binding across modules and to audit members

An attribute path may cross module boundaries, including into the platform's System module — the association does not need to live in the same module as the entity it targets:

create association IT.Issue_Assignee from IT.Issue to System.User;

DATAVIEW dv (DataSource: $Issue) {
  DYNAMICTEXT txtAssignee (Attribute: Issue_Assignee/Name)   -- into System
  DYNAMICTEXT txtApprover (Attribute: Issue_Approver/Name)   -- into another module
}

A bare association name is qualified with the module of the entity the widget sits on. On a ComboBox that matters: its DataSource: is the option list, but A text box that holds a secret needs Password: true. It is not cosmetic: without it the field renders the value in plaintext, and before ako/mxcli#550 a describe page → exec round trip silently turned every stored password field into an ordinary one — so copying a login or change-password page lost it.

SubmitBehaviour: WhileEditing makes a text box or text area submit as the user types (after SubmitOnInputDelay milliseconds, default 300) instead of on leaving the field — the setting behind live search boxes and "on change while typing" handlers. Width: FixedWidth on a layout grid is Studio Pro's "Fixed width" container. Both are printed by describe page only when not the default, and before ako/mxcli-ledger FINDINGS #170 a describe → exec round trip reset them to the defaults. An unknown value is refused (MDL-WIDGET18).

An input widget can also traverse an association to show a value from the other side: attribute: Assoc/Attr binds the far attribute and stores the hops, which is what Studio Pro does. It works on textbox, textarea, datepicker, dropdown, checkbox and radiobuttons, and on data grid columns, with the same bare-association spelling in each. Note what it is NOT: this shows a value from the associated object, it does not make it editable through the association — for editing the other object, nest a dataview over the association instead.

Match the input widget to the attribute type — mxbuild refuses every other pairing with CE2421, and check -p --references reports it as MDL-WIDGET39: textbox → String / Integer / Long / Decimal / AutoNumber; textarea → String; datepicker → DateTime; checkbox → Boolean; radiobuttons → Boolean or Enumeration. An enumeration goes in radiobuttons or combobox, never a textbox. Do not write the classic dropdown on a React-client project (show settings → OptimizedClient: Yes, as a fresh 11.14 app has): it is CE0582 (MDL-WIDGET40); use combobox.

Association: names a reference on the containing entity, so Association: Issue_Assignee resolves against the dataview's entity, not the option list's module.

Audit members declared with the Auto* pseudo-types bind under the name you declared:

create or modify persistent entity IT.Issue ( CreatedDate: AutoCreatedDate );
DYNAMICTEXT txtCreated (Attribute: CreatedDate)   -- also accepts createdDate

Script Execution Note: Script execution stops on the first error. If a page fails to create (e.g., invalid widget syntax), earlier statements in the script will have already been committed. Plan scripts with uncertain syntax in phases.

Tips

  1. OR REPLACE: Use to recreate existing pages
  2. Widget Names: Required - use descriptive camelCase names
  3. Layout Requirement: Layout must exist in the project
  4. Nesting: Use { } blocks for all widget children
  5. Properties: Use (key: value) syntax for all widget properties
  6. Bindings: Use attribute: for attributes, datasource: for data, action: for buttons

Related Commands

  • alter page Module.PageName { ... } - Modify page widgets in-place (SET, INSERT, DROP, REPLACE)
  • alter snippet Module.SnippetName { ... } - Modify snippet widgets in-place
  • describe page Module.PageName - View page source in MDL format (shows Class, Style, DesignProperties)
  • describe snippet Module.SnippetName - View snippet source in MDL format
  • list pages [in module] - List all pages
  • list widgets [where ...] [in module] - Discover widgets across pages/snippets
  • update widgets set ... where ... [dry run] - Bulk update widget properties (see below)
  • drop page Module.PageName - Delete a page

Bulk Widget Updates

Use update widgets to change properties across many widgets at once:

mdl 1;
-- Preview changes first (always use DRY RUN)
update widgets set 'Class' = 'card' where widgettype like '%Container%' in MyModule dry run;

-- Apply changes
update widgets set 'showLabel' = false where widgettype like '%combobox%';

-- Multiple properties
update widgets set 'Class' = 'btn-lg', 'Style' = 'margin-top: 8px;' where widgettype like '%ActionButton%';

PLUGGABLEWIDGET Escape Hatch

All shorthand widgets (IMAGE, COMBOBOX, GALLERY, DATAGRID, etc.) are pluggable widgets under the hood. When the shorthand doesn't expose a property you need, use pluggablewidget 'widget.id' name (properties) for full access to all widget properties.

-- Shorthand (common properties only)
image imgLogo (Image: 'MyFirstModule.Images._1', width: 48, height: 48)

-- Full PLUGGABLEWIDGET syntax (all properties available)
pluggablewidget 'com.mendix.widget.web.image.Image' imgLogo (
  datasource: imageUrl, imageUrl: 'img/logo.svg',
  widthUnit: pixels, width: 48, heightUnit: pixels, height: 48
)

The project's own widgets are documented as a skill: read .ai-context/skills/widgets/SKILL.md (also at .claude/skills/widgets/SKILL.md) for the index, then the per-widget file for the one you are placing — it carries the full property table with enumeration values, nested object properties, child slots and object lists.

mxcli widget docs -p app.mpr regenerates it (so does refresh catalog), and mxcli widget describe <name> -p app.mpr reads the same data live from the .mpk when a widget has been upgraded since.

See Also