ALTER PAGE / ALTER SNIPPET - Modify Existing Pages and Snippets

SkillDev tools

Lets your agent edit an existing page's widgets in place, change captions, styles, add, remove or reorder widgets.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the ALTER PAGE / ALTER SNIPPET - Modify Existing Pages and Snippets skill

About this capability

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.

What this skill tells your AI

The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/alter-page/SKILL.md and read by ahel’s review.

Overview

ALTER PAGE and ALTER SNIPPET modify an existing page or snippet's widget tree in-place without requiring a full create or replace. Operations work directly on the raw BSON tree, preserving widget types and properties that MDL doesn't explicitly model.

When to Use

ScenarioUse
Change a button caption, label, or stylealter page with set
Add a field to an existing formalter page with insert
Remove unused widgetsalter page with drop
Replace a footer or sectionalter page with replace
Several related changes on the same pagealter 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 scratchcreate or replace page
Create a new pagecreate 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 replace page — redefining the full page structure.

Syntax

alter page Module.PageName {
  operation1;
  operation2;
  ...
};

alter snippet Module.SnippetName {
  operation1;
  operation2;
  ...
};

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 inserted.

-- 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.Name;
  insert after dgProducts.Lifecycle {
    column NewCol (attribute: Sku, caption: 'SKU')
  };
  drop widget dgProducts.OldCol
};

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:

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

-- 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 = microflow Module.ACT_Other on btnSave
set Action = SAVE_CHANGES CLOSE_PAGE on btnSave
set Action = SHOW_PAGE Module.DetailPage on btnEdit

-- 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.

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:

PropertyWidget TypesValue TypeExample
ActionWidgets with an on-click action (ACTIONBUTTON, LINKBUTTON, clickable containers)Any create page action expressionset Action = microflow M.ACT_Go on btnSave
captionACTIONBUTTON, LINKBUTTONStringset caption = 'Submit' on btnSave
contentDYNAMICTEXTStringset content = 'New Heading' on txtTitle
labelTEXTBOX, TEXTAREA, DATEPICKER, COMBOBOX, CHECKBOX, RADIOBUTTONSStringset label = 'full Name' on txtName
buttonstyleACTIONBUTTON, LINKBUTTONPrimary, Default, Success, Danger, Warning, Infoset buttonstyle = danger on btnDelete
classAny widgetCSS class stringset class = 'card mx-2' on container1
styleAny widget (see warning below)Inline CSS stringset style = 'padding: 16px;' on container1
editableInput widgetsStringset editable = 'Never' on txtReadOnly
visibleAny widgetString or Booleanset visible = false on txtHidden
NameAny widgetStringset Name = 'newName' on oldName
TitlePage-level only (case-sensitive)Stringset Title = 'Edit Customer'
DocumentationPage-level only (case-sensitive)String ('' clears)set Documentation = 'Coordinator triage step.'
layoutPage-level onlyQualified nameset layout = Atlas_Core.Atlas_Default
PopupWidthPage-level only (case-sensitive)Positive integer (pixels)set PopupWidth = 800
PopupHeightPage-level only (case-sensitive)Positive integer (pixels)set PopupHeight = 480
PopupResizablePage-level only (case-sensitive)Booleanset PopupResizable = true
ClassPage-level (case-sensitive, no ON)CSS class stringset Class = 'container-fluid bg-light'
StylePage-level (case-sensitive, no ON)Inline CSS stringset Style = 'min-height: 100vh'
Visible (conditional)Any widget[expression]set Visible = [Name != ''] on ctnDetails
Editable (conditional)Input widgets[expression]set Editable = [Active] on txtName
'quotedProp'Pluggable widgetsString, Boolean, Numberset 'showLabel' = false on cbStatus

Conditional visibility/editability — set Visible = [expr] on widget (and Editable) attach a per-object expression. Bare attributes are rooted in the widget data context automatically: [Name != ''] becomes $currentObject/Name != '' (paths you write with $currentObject/…/$Param/… pass through). 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:

-- 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:

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

-- 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: 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

-- Drop a single widget
drop widget txtUnused

-- Drop multiple widgets
drop widget txtOldField, lblOldLabel, container2

Removes widgets and their entire subtree from the page.

REPLACE - Replace Widget Subtree

-- Replace a single widget with new content
replace footer1 with {
  footer newFooter {
    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.

DataGrid Column Operations

DataGrid2 columns are addressable using dotted notation: gridName.columnName. The column name is derived from the attribute short name or caption (same as shown by describe page).

-- SET a column property
set caption = 'Product SKU' on dgProducts.Code

-- DROP a column
drop widget dgProducts.OldColumn

-- INSERT a column after an existing one
insert after dgProducts.Price {
  column Margin (attribute: Margin, caption: 'Margin')
}

-- REPLACE a column
replace dgProducts.Description with {
  column Notes (attribute: Notes, caption: 'Notes')
}

To discover column names, run describe page Module.PageName and look at the COLUMN names inside the DATAGRID.

Troubleshooting: column operation succeeds but does nothing If an ALTER targeting a DataGrid column completes without error but makes no change, the column name used in the statement didn't match any column. The most common cause is a mismatch between what DESCRIBE shows and what ALTER resolves internally. Derivation rules:

  • Attribute-bound column → short attribute name (last segment after .): Module.Entity.Description → Description
  • Caption-only column → sanitized caption (non-alphanumeric replaced with _, leading/trailing _ trimmed): "Order Status" → Order_Status
  • Caption with only special chars (e.g. "---") → falls back to col1, col2, … (1-based index)

If the column name you copied from DESCRIBE still doesn't work, check whether the column has an attribute binding — attribute names take priority over captions.

The authored column colFoo (...) name is NOT how you address it. A column carries no stored name in the Mendix model, so the name you wrote in create page is dropped on write — always address a column by its derived name (the one describe page shows). Using the authored name now fails with an error that lists the available column names, rather than a bare "not found".

Duplicate captions are ambiguous and rejected. Two dynamic-text (or custom-content) columns with the same caption derive the same name, so ON "Amount" can't tell them apart. mxcli now refuses the operation with an ambiguity error instead of silently mutating the first and leaving the second unreachable. Give such columns distinct captions to address them individually. (Non-attribute column handles are the caption, so set Caption = ... also renames the handle — plan multi-step caption edits accordingly.)

ADD Variables - Add a Page Variable

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

drop variables $showStockColumn

Removes a page variable by name.

SET Layout - Change Page Layout

-- 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

alter page MyModule.Customer_Edit {
  set (caption = 'Save & Close', buttonstyle = success) on btnSave
};

Add a field to a form

alter page MyModule.Customer_Edit {
  insert after txtEmail {
    textbox txtPhone (label: 'Phone', attribute: Phone)
  }
};

Add a page variable for column visibility

alter page MyModule.ProductOverview {
  add variables $showStockColumn: boolean = 'if (3 < 4) then true else false'
};

Remove unused fields and update title

alter page MyModule.Customer_Edit {
  set title = 'Edit Customer Details';
  drop widget txtLegacyField, lblOldNote;
  set label = 'Email Address' on txtEmail
};

Replace a footer section

alter page MyModule.Customer_Edit {
  replace footer1 with {
    footer newFooter {
      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

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

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 the name you write in MDL is dropped:

create or replace page Mod.P (...) {
  datagrid dg1 (datasource: database Mod.Item) {
    column colLabel (attribute: Label, caption: 'The Label')   -- "colLabel" is not stored
  }
};

describe page shows that column as Label, and that is the name ALTER PAGE answers to:

alter page Mod.P { SET Caption = 'Renamed' ON dg1.colLabel }   -- WRONG: column not found
alter page Mod.P { SET Caption = 'Renamed' ON dg1.Label }      -- correct

The derived name is, in order: the bound attribute's short name, else the sanitized caption, else colN by position. mxcli check reports MDL-WIDGET16 when the name you wrote differs from the one that will address the column, so you find out at authoring time rather than from a failed ALTER.

Two columns that derive the same name are ambiguous and ALTER refuses rather than picking one — give them distinct captions.

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.

Expression-valued properties take a Mendix expression, not a literal. DynamicCellClass and Visible are expressions, so a literal CSS class has to be a quoted string inside the expression — doubled quotes in MDL:

-- WRONG: the expression becomes a bare identifier, mxbuild reports CE0117
alter page Mod.P { SET DynamicCellClass = 'highlight' ON dg1.Label }

-- correct: the expression is the string literal 'highlight'
alter page Mod.P { SET DynamicCellClass = '''highlight''' ON dg1.Label }

This applies equally to create page; the two paths behave identically. A bare identifier is not a valid Mendix expression, and mxbuild reports CE0117 against the column.

Properties holding a structured value — attribute, filter, content, actions — cannot be set by ALTER at all. It refuses them and points at create or replace 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

MistakeFix
Missing on widgetName for widget SETAdd on widgetName (only page-level properties — Title, Documentation, PopupWidth, PopupHeight, PopupResizable, Class, Style — omit ON)
unsupported page-level property: titlePage-level property names are case-sensitive — use Title, PopupWidth, PopupHeight, PopupResizable, Class, Style
Using unquoted pluggable property namesQuote pluggable props: set 'showLabel' = false on cb
pluggable property "X" not foundThe widget does not declare it — casing is not the problem (any casing resolves). The error lists the keys it does declare; describe widget <type> or describe page shows them in context. Run mxcli check … --references to get this before the script runs
Wrong widget nameUse describe page Module.Name to see widget names
SET on non-existent widgetWidget names are case-sensitive; check with DESCRIBE
Missing semicolons between operationsEach operation inside { } ends with ;

Limitations — prefer binding at page creation (ledger finding #45)

ALTER PAGE is best for content edits (add/remove/retitle widgets). Three things it cannot do; when you hit them, 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 = microflow … on btnSave is a parse error. Set the button's action when the button is created (or REPLACE the button subtree).

  2. REPLACE cannot reuse a widget name that lives inside the subtree being replaced. The replacement is built (registering its widget names) before the old subtree is removed, so reusing e.g. btnSave collides with the still-present old btnSave ("duplicate widget name 'btnSave'"). Give the replacement widgets fresh names, or rebuild the whole page with create or replace page.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
122
Forks
49
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
alter-page
Source
github.com/mendixlabs/mxcli