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.
Pro scans all 3 files and shows the line behind each finding
Scanned 10/5/2026
npx -y skills add mendixlabs/mxcli --skill create-page --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Create Page?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mendixlabs-create-page)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: create-page
description: "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."
---
# 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`](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`](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
```sql
create [or replace] page Module.PageName
(
[params: ( $ParamName: Module.EntityType | PrimitiveType, ... ),]
[variables: ( $varName: DataType = 'defaultExpression', ... ),]
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)` |
| 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)` |
| 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:
```sql
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:
```sql
container c (class: 'card mx-spacing-top-large') { ... }
actionbutton btn (caption: 'Save', class: 'btn-lg')
```
**Inline Style** — One-off CSS styles (use sparingly):
```sql
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):
```sql
-- 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`.
```sql
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:**
```sql
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
```sql
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
```sql
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.
```sql
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.
```sql
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](../choose-edit-mode/SKILL.md).
See the dedicated skill file: [ALTER PAGE/SNIPPET](../alter-page/SKILL.md)
## 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`:
```sql
-- 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:
```sql
container cntRunning (visible: Status in (Running, empty)) { ... }
textbox txtPassword (label: 'Password', attribute: Password, visible: IsLocalUser in (true))
```
> **`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](https://docs.mendix.com/refguide/xpath-constraint-functions/),
> [XPath keywords](https://docs.mendix.com/refguide/xpath-keywords-and-system-variables/).
> **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](../xpath-constraints/SKILL.md)).
> - **Microflow expression**: qualified value —
> `$obj/Status = MES.EquipmentStatus.Running` (see [write-microflows](../write-microflows/SKILL.md)).
>
> 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:
> ```sql
> -- 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`:
```sql
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:
```sql
image imgRemote (ImageType: imageUrl, ImageUrl: 'https://example.com/logo.svg')
image imgIcon (ImageType: icon)
```
### A button's icon is one of three elements
Mendix stores **three different icon elements**, and the keyword picks which:
```sql
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`
reports it as **MDL078** first. 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:
```sql
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.
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:
```sql
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:
```sql
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.
```sql
-- 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
- [Overview Pages](../overview-pages/SKILL.md) - CRUD page patterns
- [Master-Detail Pages](../master-detail-pages/SKILL.md) - Selection binding pattern
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!