Modify an existing page or snippet's widget tree in place with ALTER PAGE / ALTER SNIPPET — SET, INSERT, DROP, REPLACE and SET Layout. Use when changing a caption, style or property, adding or removing a widget, or reordering a form, instead of rewriting the whole page with CREATE OR REPLACE. The only safe way to change a page or snippet authored in Studio Pro.
Scanned 10/4/2026
npx -y skills add mendixlabs/mxcli --skill alter-page --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Alter Page?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mendixlabs-alter-page)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: alter-page
description: "Modify an existing page or snippet's widget tree in place with ALTER PAGE / ALTER SNIPPET — SET, INSERT, DROP, REPLACE and SET Layout. Use when changing a caption, style or property, adding or removing a widget, or reordering a form, instead of rewriting the whole page with CREATE OR REPLACE. The only safe way to change a page or snippet authored in Studio Pro."
---
# ALTER PAGE / ALTER SNIPPET - Modify Existing Pages and Snippets
## Overview
ALTER PAGE and ALTER SNIPPET modify an existing page or snippet's widget tree **in-place** without requiring a full `create or modify`. Operations work directly on the raw BSON tree, preserving widget types and properties that MDL doesn't explicitly model.
## When to Use
| Scenario | Use |
|----------|-----|
| Change a button caption, label, or style | `alter page` with `set` |
| Add a field to an existing form | `alter page` with `insert` |
| Remove unused widgets | `alter page` with `drop` |
| Replace a footer or section | `alter page` with `replace` |
| Several related changes on the same page | `alter page` with multiple operations in one block |
| Same property across many pages (e.g., add `Class` to every Container) | `update widgets` — see `bulk-widget-updates` |
| Rebuild entire page from scratch (pages your scripts own only) | `create or modify page` |
| Create a new page | `create page` |
**Rule of thumb:**
- `alter page` — targeted edits to one page. Combine multiple ops in one block when they belong together.
- `update widgets` — cross-page bulk updates with `WHERE` filtering and `DRY RUN`.
- `create or modify page` — redefining the full page structure of a page your MDL scripts own.
**Who owns the page decides** ([choose-edit-mode](../choose-edit-mode/SKILL.md)). A page
or snippet authored or edited in Studio Pro is changed with `alter`, however large the
change. `describe` → edit → `create or modify` is only for pages your
MDL scripts created and nobody has changed in Studio Pro since: on a Studio Pro page that
round trip has dropped translations and filled an empty English caption from another
language, and on a snippet it has dropped the snippet's type.
## Syntax
```sql
alter page Module.PageName {
operation1;
operation2;
...
};
alter snippet Module.SnippetName {
operation1;
operation2;
...
};
```
This is the **generic ALTER** — the same four verbs for every document type
(`alter layout` too):
```sql
alter page Module.PageName {
set (Key: value, …) on <target>; -- no `on`: the page itself
insert before|after|into <target> { <widgets as in create page> }
replace <target> with { <widgets> }
drop <target>, <target>;
};
```
A `<target>` on a page is a widget name, a DataGrid 2 column `grid column(Attr)`
(or `grid column('Caption')`), or a layout region `layoutContainer.top`. Properties go in parentheses with `:`, exactly as in
`create page`. The older spellings `set Key = value on w`, `set Key: value`
(no parentheses) and `drop widget w` still run, and warn with MDL-DEPR101,
MDL-DEPR102 and MDL-DEPR103 — write the form above.
Multiple operations can be combined in a single ALTER statement. They are applied sequentially; later operations see the page state produced by earlier ones, so you can `set` on a widget you just `insert`ed.
```sql
mdl 1;
-- Rename a column, add a sibling, drop an obsolete one — all in one block.
alter page MyMod.Product_Overview {
set (caption: 'Product Name') on dgProducts column(Name);
insert after dgProducts column(Lifecycle) {
column (attribute: Sku, caption: 'SKU')
};
drop dgProducts column('Old')
};
```
For changes that should be applied across **many pages** (e.g., "add `Class='card'` to every Container in `MyMod`"), use `UPDATE WIDGETS` instead — see `bulk-widget-updates`.
## Operations
### List View Specialization Templates
A List View template has no name, so it cannot be reached by a widget ref like
every other target. Adding one reuses `INSERT INTO` with the same
`template for` block `create page` uses — a template has one spelling
everywhere. Removing one has its own form:
```sql
mdl 1;
alter page Pages.Vehicle_Overview {
insert into vehicleListView {
template for Pages.Motorcycle {
dynamictext mcLabel (content: 'Motorcycle {1}', contentparams: ({1} = Brand))
}
};
drop template for Pages.SUV in vehicleListView
};
```
Naming the list view in the `drop` is required, not optional: one page can hold
two list views with a template for the same entity.
Most template edits need none of this. The widgets **inside** a template are
ordinary named widgets, so `set (content: '…') on busLabel` and
`insert after busLabel { … }` already work and land in the right template. To
replace a whole template, `drop` it and `insert` the new one in the same block —
operations apply in order.
Refused, each naming the problem:
- `insert before` / `insert after` a template — templates are not siblings of the
widgets in the list view's body, so only `insert into` makes sense.
- mixing `template for …` blocks with ordinary widgets in one `insert` — they go
to different places (the Templates array and the default body). Use two inserts.
- a template for an entity that is not the list view's entity or a specialization
of it — it could never match an object the list view shows.
- a second template for an entity that already has one.
- `drop template for` an entity with no template — the error names the ones that
are there, because dropping nothing and reporting success is how a typo becomes
a silent no-op.
### SET - Modify Widget Properties
```sql
-- Single property
set (caption: 'New Caption') on widgetName
-- Multiple properties
set (caption: 'Save & Close', buttonstyle: success) on btnSave
-- Page-level property (no ON clause). Page-level property names are
-- case-sensitive and must match the Mendix property exactly.
set (Title: 'New Page Title')
-- Pop-up dimensions (apply when the page is opened in a pop-up)
set (PopupWidth: 800)
set (PopupHeight: 480)
set (PopupResizable: true)
set (Documentation: 'What this page is for.')
-- Retarget a button's on-click action. Any form `create page` accepts works
-- here, including the combined ones.
set (Action: call microflow Module.ACT_Other) on btnSave
set (Action: SAVE CHANGES CLOSE PAGE) on btnSave
set (Action: SHOW PAGE Module.DetailPage) on btnEdit
-- Retarget ONE named action slot of a pluggable widget, by the widget's own
-- property key (the same key `create page` takes: `createFileAction: …`).
set ('createFileAction': call microflow Module.ACT_CreateFile) on fileUploader1
set ('onSelectionChange': show page Module.Detail) on dgOrders
-- Rebind a data-bound widget
set (DataSource: $OrderParam) on dvOrder
set (DataSource: microflow Module.MF_Get) on dvOrder
```
**Prefer `set Action` over `replace` when only the action changes.** `replace`
rebuilds the widget from what the statement says, so any property you do not
restate — `ButtonStyle`, `Class`, design properties, tooltip — is dropped. `set`
edits the one property and leaves the rest of the widget alone.
The exception is a **pluggable widget replaced by one of the same kind** (a combo
box by a combo box): there `replace` keeps every stored property the statement
does not change — a translated placeholder, `readOnlyStyle`, anything MDL has no
word for — and writes only what differs from the widget as `describe` prints it.
So a sort can be added to a combo box's options by restating its `describe`
line with `sort by` appended.
`set Action` is refused on a widget that has no action (a plain container, say),
rather than writing a property the widget type does not define — Studio Pro
refuses to open a document with an unknown property while MxBuild tolerates it,
so a silent write would build cleanly and then fail to open.
**Supported SET properties:**
| Property | Widget Types | Value Type | Example |
|----------|-------------|------------|---------|
| `Action` | Widgets with an on-click action (ACTIONBUTTON, LINKBUTTON, clickable containers) | Any `create page` action expression | `set (Action: call microflow M.ACT_Go) on btnSave` |
| `'<slotKey>'` | Pluggable widgets — any **action-typed** property (File Uploader `createFileAction`, DataGrid 2 `onSelectionChange`, …) | Any `create page` action expression | `set ('createFileAction': call microflow M.ACT_Create) on fileUploader1` — refused, naming the widget's action slots, if the key is not action-typed |
| `caption` | ACTIONBUTTON, LINKBUTTON | String | `set (caption: 'Submit') on btnSave` |
| `content` | DYNAMICTEXT | String | `set (content: 'New Heading') on txtTitle` |
| `label` | TEXTBOX, TEXTAREA, DATEPICKER, COMBOBOX, CHECKBOX, RADIOBUTTONS | String | `set (label: 'full Name') on txtName` |
| `buttonstyle` | ACTIONBUTTON, LINKBUTTON | Primary, Default, Success, Danger, Warning, Info | `set (buttonstyle: danger) on btnDelete` |
| `class` | Any widget | CSS class string | `set (class: 'card mx-2') on container1` |
| `style` | Any widget (see warning below) | Inline CSS string | `set (style: 'padding: 16px;') on container1` |
| `editable` | Input widgets | String | `set (editable: 'Never') on txtReadOnly` |
| `visible` | Any widget | String or Boolean | `set (visible: false) on txtHidden` |
| `Name` | Any widget | String | `set (Name: 'newName') on oldName` |
| `Title` | Page-level only (case-sensitive) | String | `set (Title: 'Edit Customer')` |
| `Documentation` | Page-level only (case-sensitive) | String (`''` clears) | `set (Documentation: 'Coordinator triage step.')` |
| `layout` | Page-level only | Qualified name | `set layout = Atlas_Core.Atlas_Default` |
| `PopupWidth` | Page-level only (case-sensitive) | Positive integer (pixels) | `set (PopupWidth: 800)` |
| `PopupHeight` | Page-level only (case-sensitive) | Positive integer (pixels) | `set (PopupHeight: 480)` |
| `PopupResizable` | Page-level only (case-sensitive) | Boolean | `set (PopupResizable: true)` |
| `Class` | Page-level (case-sensitive, no ON) | CSS class string | `set (Class: 'container-fluid bg-light')` |
| `Style` | Page-level (case-sensitive, no ON) | Inline CSS string | `set (Style: 'min-height: 100vh')` |
| `Visible` (conditional) | Any widget | expression | `set (Visible: $currentObject/Name != '') on ctnDetails` |
| `Editable` (conditional) | Input widgets | expression | `set (Editable: $currentObject/Active) on txtName` |
| `'quotedProp'` | Pluggable widgets | String, Boolean, Number | `set ('showLabel': false) on cbStatus` |
> **Conditional visibility/editability** — `set (Visible: <expr>) on widget` (and
> `Editable`) attach a per-object client expression, stored as written: name
> attributes as `$currentObject/Name`. The bracketed `set (Visible: [Name != ''])`,
> which roots a bare attribute for you, still works and warns MDL-DEPR081. Setting
> `Editable` on a non-input widget is rejected. This mirrors CREATE PAGE's
> `visible:` — see the create-page skill for enum-value rules.
**Pluggable widget properties** use quoted names to set values in the widget's `Object.Properties[]`. Boolean values are stored as `"yes"`/`"no"` in BSON.
**Column property names are case-insensitive** in MDL — `set (caption: …)` and `set (Caption: …)` both work. The internal BSON keys are dictated by the widget schema and stay case-sensitive on the storage side.
> **Warning: Style on DYNAMICTEXT** — Setting `style` directly on a DYNAMICTEXT widget crashes MxBuild with a NullReferenceException. Wrap the DYNAMICTEXT in a CONTAINER and apply styling to the container instead:
> ```sql
> -- Wrong: crashes MxBuild
> SET (Style: 'color: red;') ON txtHeading
>
> -- Correct: style the container
> REPLACE txtHeading WITH {
> CONTAINER ctnHeading (Style: 'color: red;') {
> DYNAMICTEXT txtHeading (Content: 'Heading', RenderMode: H2)
> }
> }
> ```
#### Changing a widget's DataSource
`SET DataSource` retypes a data source in place — including across shapes, e.g.
from a microflow to a page parameter:
```sql
mdl 1;
ALTER PAGE MyModule.OrderPage {
SET (DataSource: $Order) ON dvOrder; -- page/snippet parameter
SET (DataSource: microflow MyModule.MF_Get) ON dvOrder; -- microflow
SET (DataSource: nanoflow MyModule.NF_Get) ON dvOrder; -- nanoflow
SET (DataSource: selection dgOrders) ON dvDetail; -- listen to widget
};
```
The parameter must exist on the page (or snippet) being altered — its entity is
read from the container's own parameter list, and an unknown name is refused
rather than written as an unresolved reference.
`association` and `database` sources are **not** supported by SET. Use REPLACE
for those, which rebuilds the widget through the CREATE PAGE path and handles
every datasource type; the error message says so.
A `database` source has no single stored shape — the widget holding it decides
which element Mendix writes (a list view, a data grid and a pluggable widget
each store a different one), and SET writes the property directly rather than
rebuilding the widget, so it has nothing to choose from. This used to be
accepted and half-applied: the widget was left with a source that DESCRIBE read
back as absent and mxbuild rejected as **CE7007**, on a page `exec` had just
reported as altered (mendixlabs/mxcli#1032).
A **data view** is the one case REPLACE does not rescue: it binds to a single
object, so Mendix gives it no database form at all and the CREATE PAGE path
refuses one too. Point it at a context parameter, a microflow, a nanoflow or
`selection <widget>`, and use a list view or a data grid to show a query. The
refusal says which of the two situations you are in.
### INSERT - Add Widgets
```sql
-- Insert after a widget
insert after txtName {
textbox txtMiddleName (label: 'Middle Name', attribute: MiddleName)
}
-- Insert before a widget
insert before btnSave {
actionbutton btnPreview (caption: 'Preview', action: call microflow Module.ACT_Preview)
}
-- Insert INTO a container — append as its last child (works on an EMPTY container)
insert into ctnToolbar {
actionbutton btnNew (caption: 'New', action: nothing, buttonstyle: primary)
}
```
Inserted widgets use the same syntax as `create page`. Multiple widgets can be inserted in a single block.
`insert into <container>` appends as the last child of the named container — the
only way to fill an **empty** container, and handy for adding to a container/dataview
without needing a sibling to anchor to. Widgets inserted into a dataview take that
dataview's entity as their context. Supported on simple containers (container,
dataview, groupbox, scroll-container region); for a layout grid or tab container,
insert relative to a widget inside the target column/tab instead.
**The context comes from the nearest enclosing data source, whatever kind it is**
— a database or association source, a microflow/nanoflow source (the entity is
the flow's return type), or `datasource: selection <list>`, which takes the
entity of the list it listens to. A bare attribute in the inserted or replaced
widget resolves against that entity, exactly as it would in `create page`. When
no enclosing source can be resolved, the binding is written unset rather than
guessed at — `describe page` then prints `<unbound>`, and mxbuild reports
`CE0402 "No value specified."`, so re-describe the page after an ALTER that
moves data-bound widgets.
### DROP - Remove Widgets
```sql
-- Drop a single widget
drop txtUnused
-- Drop multiple widgets
drop txtOldField, lblOldLabel, container2
```
Removes widgets and their entire subtree from the page.
### REPLACE - Replace Widget Subtree
```sql
mdl 1;
-- Replace a data view's footer (it has no name: address it by its data view)
alter page MyModule.Customer_Edit {
replace dvMain.footer with {
footer {
actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary)
actionbutton btnCancel (caption: 'Cancel', action: cancel changes)
}
}
};
```
Replaces the target widget with one or more new widgets. The new widgets use the same syntax as `create page`, and may reuse the names of the widgets the replace removes. `insert into dvMain.footer { … }` appends to a footer and `drop dvMain.footer` empties it.
### DataGrid Column Operations
A DataGrid 2 column is addressed by what `describe page` prints in it: `grid column(Attr)` for its `attribute:`, `grid column('Caption')` for its `caption:`. See "DataGrid 2 columns: how to address them" below.
```sql
-- SET a column property
set (caption: 'Product SKU') on dgProducts column(Code)
-- DROP a column
drop dgProducts column('Old column')
-- INSERT a column after an existing one
insert after dgProducts column(Price) {
column (attribute: Margin, caption: 'Margin')
}
-- REPLACE a column
replace dgProducts column(Description) with {
column (attribute: Notes, caption: 'Notes')
}
-- Two columns over one attribute: pick one with @n
drop dgProducts column(Name)@2
```
The older dotted form `gridName.columnName` still works; it matches a name mxcli derives (the short attribute name, else the sanitized caption, else `colN`).
### ADD Variables - Add a Page Variable
```sql
add variables $showStockColumn: boolean = 'true'
```
Adds a new page variable (`Forms$LocalVariable`) to the page/snippet. DataType can be `boolean`, `string`, `integer`, `decimal`, `datetime`, or an entity type. Default value is a Mendix expression in single quotes.
### DROP Variables - Remove a Page Variable
```sql
drop variables $showStockColumn
```
Removes a page variable by name.
### SET Layout - Change Page Layout
```sql
-- Auto-map placeholders by name (most common case)
set layout = Atlas_Core.Atlas_Default
-- Explicit mapping when placeholder names differ
set layout = Atlas_Core.Atlas_SideBar map (Main as content, Extra as Sidebar)
```
Changes the page's layout without rebuilding the widget tree. Only rewrites the `FormCall.Form` and `FormCall.Arguments[].Parameter` BSON fields — all widget content is preserved. Not supported for snippets.
When placeholders have the same names in both layouts (e.g., both have `Main`), auto-mapping works. Use `map` when placeholder names differ between the old and new layout.
## Examples
### Change button text and style
```sql
mdl 1;
alter page MyModule.Customer_Edit {
set (caption: 'Save & Close', buttonstyle: success) on btnSave
};
```
### Add a field to a form
```sql
mdl 1;
alter page MyModule.Customer_Edit {
insert after txtEmail {
textbox txtPhone (label: 'Phone', attribute: Phone)
}
};
```
### Add a page variable for column visibility
```sql
mdl 1;
alter page MyModule.ProductOverview {
add variables $showStockColumn: boolean = 'if (3 < 4) then true else false'
};
```
### Remove unused fields and update title
```sql
mdl 1;
alter page MyModule.Customer_Edit {
set (title: 'Edit Customer Details');
drop txtLegacyField, lblOldNote;
set (label: 'Email Address') on txtEmail
};
```
### Replace a footer section
```sql
mdl 1;
alter page MyModule.Customer_Edit {
replace dvMain.footer with {
footer {
actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: success)
actionbutton btnDelete (caption: 'Delete', action: delete, buttonstyle: danger)
actionbutton btnCancel (caption: 'Cancel', action: cancel changes)
}
}
};
```
### Modify a snippet
```sql
mdl 1;
alter snippet MyModule.NavigationMenu {
set (caption: 'Dashboard') on btnHome;
insert after btnHome {
actionbutton btnReports (caption: 'Reports', action: show page MyModule.Reports_Overview)
}
};
```
### Set pluggable widget properties
```sql
mdl 1;
alter page MyModule.Customer_Edit {
set ('showLabel': false) on cbStatus;
set ('labelWidth': 4) on cbCategory
};
```
## DataGrid 2 columns: how to address them, and what you can set
**Mendix stores no column name.** A DataGrid 2 column's schema has no name or
identifier key — the only human-facing label is its caption — so a column is
written without one, and `describe page` prints none:
```mdl
create or modify page Mod.P (...) {
datagrid dg1 (datasource: database Mod.Item) {
column (attribute: Label, caption: 'The Label')
column (caption: 'Actions', ShowContentAs: customContent) { … }
}
};
```
A name written there anyway (`column colLabel (…)`, the old describe output) is
dropped and reported as **MDL-DEPR005**; `mxcli fmt --upgrade` removes it.
`ALTER PAGE` addresses a column by **what describe prints in it** — its
`attribute:` value, or its `caption:`:
```mdl
mdl 1;
alter page Mod.P { set (Caption: 'Renamed') on dg1 column(Label) }; -- the column bound to Label
alter page Mod.P { drop dg1 column('Actions') }; -- the column captioned Actions
alter page Mod.P { set (Sortable: false) on dg1 column(Owner/Name) }; -- over an association, as describe writes it
```
Two columns over the same attribute (or with the same caption) share the
address. ALTER refuses it rather than picking one, and the error lists the
matches — add `@n` to choose: `drop dg1 column(FullName)@2`.
The older `dg1.Label` form still works: it addresses a column by a name mxcli
derives (the attribute's short name, else the sanitized caption, else `colN` by
position). Prefer `column(…)`, which says what it matches.
### Setting column properties
Property names resolve against the keys the installed widget declares, so both
the schema key and mxcli's MDL alias work (`DynamicCellClass` and `ColumnClass`
both reach `columnClass`). An unknown name lists what *is* settable on that grid.
**`DynamicCellClass` (and a widget's `DynamicClasses`) take a Mendix expression,
written as-is.** A quoted value is a Mendix string, so a literal CSS class is just
the quoted class name, and a computed one is the expression itself:
```mdl
mdl 1;
-- a literal class: the string 'highlight'
alter page Mod.P { SET (DynamicCellClass: 'highlight') ON dg1 column(Label) };
-- a computed class
alter page Mod.P { SET (DynamicCellClass: if $currentObject/Price > 100 then 'highlight' else '') ON dg1 column(Label) };
```
```text
-- WRONG: a bare name is an identifier, not a string — mxbuild reports CE0117
alter page Mod.P { SET (DynamicCellClass: highlight) ON dg1 column(Label) };
```
The old spelling — the expression's text in quotes, `'if … then ''a'' else '''''`
— is refused under `mdl 1;` as MDL-WIDGET33, because there it stores that text
as a class name. A script without the header keeps its old meaning and warns
MDL-V1-QUOTEDEXPR; `mxcli fmt --upgrade` writes it bare. This applies equally to
`create page`; the two paths behave identically.
A column's pluggable `Visible` expression is not converted yet: there a quoted
value is still the expression's text, so a literal needs the doubled quotes.
Properties holding a **structured** value — `attribute`, `filter`, `content`,
actions — cannot be set by ALTER at all. It refuses them and points at
`create or modify page`, rather than writing a string where Mendix expects a
reference.
**Widget property names are matched case-insensitively**, pluggable ones
included, so a spelling `CREATE PAGE` accepts is a spelling `ALTER PAGE` accepts
— `set (PageSize: 10) on dgProducts` and `set (pageSize: 10) on dgProducts` are the
same statement. This is what makes DESCRIBE output re-executable: `describe page`
prints the capitalised `PageSize:`, while the widget template stores `pageSize`
(mendixlabs/mxcli#1069). A property the widget does not declare is still an
error — and `mxcli check … --references` reports it **before** the script runs,
so a typo no longer lands halfway through. The pre-flight resolves the name
against the stored document rather than a list, so it is right about whatever
widget package this project has installed; the error names the widget's own
property keys. `ON` a widget the page does not have is caught the same way.
Two things it deliberately stays quiet about, because it cannot answer them: a
page the script itself creates (nothing is stored yet — the widgets there are
checked where they are written), and a widget an `INSERT` in the same script
adds. Both still fail at exec if they are genuinely wrong.
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Missing `on widgetName` for widget SET | Add `on widgetName` (only page-level properties — `Title`, `Documentation`, `PopupWidth`, `PopupHeight`, `PopupResizable`, `Class`, `Style` — omit ON) |
| `unsupported page-level property: title` | Page-level property names are case-sensitive — use `Title`, `PopupWidth`, `PopupHeight`, `PopupResizable`, `Class`, `Style` |
| Using unquoted pluggable property names | Quote pluggable props: `set ('showLabel': false) on cb` |
| `pluggable property "X" not found` | The widget does not declare it — casing is not the problem (any casing resolves). The error lists the keys it does declare; `describe widget type <type>` or `describe page` shows them in context. Run `mxcli check … --references` to get this before the script runs |
| Wrong widget name | Use `describe page Module.Name` to see widget names |
| SET on non-existent widget | Widget names are case-sensitive; check with DESCRIBE |
| Missing semicolons between operations | Each operation inside `{ }` ends with `;` |
## Limitations — prefer binding at page creation (ledger finding #45)
`ALTER PAGE` is best for *content* edits (add/remove/retitle widgets). When you hit
the limit below, define the referenced microflows **before** the
page and bind the buttons at creation time instead of rewiring afterwards:
1. **`SET` cannot rewire a button's action.** `set` accepts a fixed property list
(`caption`, `class`, `visible`, …) — `action` is not on it, so
`set Action = call microflow … on btnSave` is a parse error. Set the button's action
when the button is created (or `REPLACE` the button subtree).
**A data view's footer has no name.** Its widgets are stored in the data
view, and the footer itself is not, so a name written on it is never kept
(MDL-DEPR005) and `describe` prints `footer { … }`. Address it by its data
view: `replace dvMain.footer with { footer { … } }`, `insert into
dvMain.footer { … }`, `drop dvMain.footer`.
**Recommended pattern**: put save/reset microflows in a file that runs *before* the
page definition, and bind the popup/footer buttons to them at creation. The
apparent "page needs microflow, microflow needs page" cycle usually exists only
between *different* microflows, not within the page itself.
## Validation Checklist
1. **Get widget names first**: Run `describe page Module.PageName` to see all widget names
2. **Check syntax**: `mxcli check script.mdl`
3. **Check references**: `mxcli check script.mdl -p app.mpr --references`
4. **Verify result**: Run `describe page Module.PageName` after ALTER to confirm changes
5. **Validate project**: `mxcli docker check -p app.mpr` (or `mxcli docker check -p app.mpr`)
## Related Commands
- `describe page Module.PageName` - View current page structure (get widget names)
- `describe snippet Module.SnippetName` - View current snippet structure
- `create [or replace] page` - Create or fully rebuild a page
- `create [or replace] snippet` - Create or fully rebuild a snippet
- `update widgets set ... where ...` - Bulk update widget properties across pages
- `drop page Module.PageName` - Delete a page
- `drop snippet Module.SnippetName` - Delete a snippet
## Related Skills
- [Create Page](../create-page/SKILL.md) - Full page creation syntax
- [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!