resolve-forward-references
Order MDL statements so that every reference resolves — execution is sequential and immediate, so a document must exist before anything points at it. Use when a script fails on a reference to somethin
SKILL.md.claude/skills/mendix/resolve-forward-references/SKILL.md
GitHub<!-- skill-index: generated by scripts/skill-index.py; make check-skill-index -->
<!-- /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 |
|---|---|
| 26-52 | Why Forward References Fail |
| 53-70 | The Placeholder Pattern |
| 71-137 | Pattern 1 — Shared Navigation Snippet (most common) |
| 138-185 | Pattern 2 — Page References Another Page (new/edit from overview) |
| 186-218 | Pattern 3 — Microflow References a Page Not Yet Created |
| 219-237 | Ordering Rules for Dependency-Free Scripts |
| 238-261 | Choosing Between CREATE OR MODIFY and ALTER SNIPPET |
| 262-336 | Script Template for a Full CRUD Module |
| 337-341 | Related Skills |
Resolving Forward References in MDL Scripts
Why Forward References Fail
MDL script execution is sequential and immediate — each CREATE statement commits
its document to the project database before the next statement runs. When a document is
being built, all its references (snippets, pages, microflows) are resolved against the
database at that moment. A reference to something defined later in the same script fails
because it is not in the database yet.
Error: snippet not found: MyModule.NavMenu
This applies to the following reference types:
| Reference | In | Fails when |
|---|---|---|
snippetcall |
page / snippet | snippet created after the page |
show page in action |
page / snippet | page created after the page that references it |
SHOW PAGE |
microflow | page created after the microflow |
call microflow |
microflow | callee created after the caller — only under exec --continue-on-error, the REPL or -c. Plain mxcli exec rolls back on failure and resolves the call against every flow the script declares, so mutual recursion needs no stub (mendixlabs/mxcli#1405) |
Note:
SHOW PAGEinside a microflow body resolves the page reference at microflow-creation time, not at invocation time. If the target page doesn't exist yet, the microflow creation fails.
The Placeholder Pattern
The standard workaround is a three-step sequence:
- Create a minimal placeholder for the document that will be referenced.
- Create all documents that reference it. They bind to the placeholder's ID.
- Fill in the placeholder using
CREATE OR MODIFYorALTER— both preserve the original ID so existing bindings remain valid.
Write
CREATE OR MODIFY, notCREATE OR REPLACE.OR REPLACEis the deprecated spelling ofOR MODIFY(MDL-DEPR001, refused frommdl 2): it means the same and also keeps the ID, so it does not break the bindings — but it is not the spelling to teach. The oneOR REPLACEthat does delete and recreate iscreate or replace view entityin a script without anmdl 1;header (MDL-V1-REPLACE01); that is not a page or snippet.
Pattern 1 — Shared Navigation Snippet (most common)
A navigation snippet contains show page buttons (references pages) and pages include
the snippet via snippetcall (references the snippet). Both sides reference each other.
mdl 1;
-- Step 1: placeholder snippet (minimal valid content)
create snippet MyModule.NavMenu
{
layoutgrid g { row { column (desktopwidth: 12) {
dynamictext loading (content: 'Loading...')
}}}
};
-- Step 2: pages that embed the snippet (snippet already exists → resolves OK)
create page MyModule.Customer_Overview
(
title: 'Customers',
layout: Atlas_Core.Atlas_Default
)
{
layoutgrid g { row {
column (desktopwidth: 3) {
snippetcall nav (snippet: MyModule.NavMenu)
}
column (desktopwidth: 9) {
datagrid dg (datasource: database MyModule.Customer) { }
}
}}
};
create page MyModule.Order_Overview
(
title: 'Orders',
layout: Atlas_Core.Atlas_Default
)
{
layoutgrid g { row {
column (desktopwidth: 3) {
snippetcall nav (snippet: MyModule.NavMenu)
}
column (desktopwidth: 9) {
datagrid dg (datasource: database MyModule.Order) { }
}
}}
};
-- Step 3: fill in the snippet with real content (pages now exist → show page resolves OK)
-- Use CREATE OR MODIFY (preserves ID) or ALTER SNIPPET (in-place)
create or modify snippet MyModule.NavMenu
{
layoutgrid g { row { column (desktopwidth: 12) {
actionbutton btnCustomers (
caption: 'Customers',
action: show page MyModule.Customer_Overview
)
actionbutton btnOrders (
caption: 'Orders',
action: show page MyModule.Order_Overview
)
}}}
};
Pattern 2 — Page References Another Page (new/edit from overview)
An overview page has a New button that opens a NewEdit page via show page. The NewEdit
page must exist before the overview can reference it.
mdl 1;
-- Solution: declare the target page first (even if empty), then the referencing page
create page MyModule.Customer_NewEdit
(
params: ( $Customer: MyModule.Customer ),
title: 'Edit Customer',
layout: Atlas_Core.PopupLayout
)
{
layoutgrid g { row { column (desktopwidth: 12) {
dataview dv (datasource: $Customer) {
textbox txtName (label: 'Name', attribute: Name)
}
actionbutton btnSave (caption: 'Save', action: save changes)
actionbutton btnCancel (caption: 'Cancel', action: cancel changes)
}}}
};
-- Now the overview can safely reference the NewEdit page
create page MyModule.Customer_Overview
(
title: 'Customers',
layout: Atlas_Core.Atlas_Default
)
{
layoutgrid g { row { column (desktopwidth: 12) {
actionbutton btnNew (
caption: 'New',
action: call microflow MyModule.ACT_Customer_New
)
datagrid dg (datasource: database MyModule.Customer) {
column (caption: 'Name', attribute: Name)
}
}}}
};
For simple cases, reordering declarations is sufficient and no placeholder is needed.
Pattern 3 — Microflow References a Page Not Yet Created
mdl 1;
-- If the page is defined later in the script, create a placeholder or reorder.
-- Easiest fix: declare the page before the microflow that shows it.
-- Page first
create page MyModule.Order_Detail
(
params: ( $Order: MyModule.Order ),
title: 'Order Detail',
layout: Atlas_Core.Atlas_Default
)
{
layoutgrid g { row { column (desktopwidth: 12) {
dataview dv (datasource: $Order) {
textbox txtID (label: 'Order ID', attribute: OrderID)
}
}}}
};
-- Microflow after the page it references
create microflow MyModule.ACT_OpenOrder ($Order: MyModule.Order)
begin
@position(200,200)
show page MyModule.Order_Detail (Order = $Order);
@position(400,200) return;
end;
Ordering Rules for Dependency-Free Scripts
To avoid forward references entirely, follow this declaration order within a script:
1. Entities and associations (no cross-document references)
2. Enumerations and constants (no cross-document references)
3. Snippets (placeholder if needed)
4. Pages (reference snippets + other pages)
5. Snippets (fill-in step, if placeholder was used)
6. Microflows and nanoflows (reference pages, entities)
7. Navigation (references pages)
When generating MDL scripts, write sections in this order. Doing so avoids the placeholder pattern for the majority of scripts.
Choosing Between CREATE OR MODIFY and ALTER SNIPPET
Both preserve the snippet's ID. Use whichever fits:
| Approach | When to use |
|---|---|
create or modify snippet |
Rewriting the whole snippet body from scratch |
alter snippet |
Inserting or replacing specific widgets within an existing layout |
mdl 1;
-- ALTER SNIPPET: targeted widget replacement (keeps surrounding structure)
alter snippet MyModule.NavMenu {
replace loading with {
actionbutton btnCustomers (
caption: 'Customers',
action: show page MyModule.Customer_Overview
)
}
};
Script Template for a Full CRUD Module
mdl 1;
-- ============================================================
-- MyModule CRUD scaffold
-- Correct declaration order: snippets → pages → microflows → nav
-- ============================================================
-- 1. Placeholder for shared navigation (will reference pages created below)
create snippet MyModule.AppNav
{
layoutgrid g { row { column (desktopwidth: 12) {
dynamictext placeholder (content: '...')
}}}
};
-- 2. NewEdit page (referenced by Overview's New button)
create page MyModule.Customer_NewEdit
(
params: ( $Customer: MyModule.Customer ),
title: 'Edit Customer',
layout: Atlas_Core.PopupLayout
)
{
-- ... widgets ...
};
-- 3. Overview page (references NewEdit + NavMenu)
create page MyModule.Customer_Overview
(
title: 'Customers',
layout: Atlas_Core.Atlas_Default
)
{
layoutgrid g { row {
column (desktopwidth: 3) {
snippetcall nav (snippet: MyModule.AppNav)
}
column (desktopwidth: 9) {
-- ... datagrid with New button calling ACT_Customer_New ...
}
}}
};
-- 4. Fill in navigation (pages now exist)
create or modify snippet MyModule.AppNav
{
layoutgrid g { row { column (desktopwidth: 12) {
actionbutton btnCustomers (
caption: 'Customers',
action: show page MyModule.Customer_Overview
)
}}}
};
-- 5. Microflows (pages already exist)
create microflow MyModule.ACT_Customer_New ()
begin
$c = create MyModule.Customer ();
show page MyModule.Customer_NewEdit (Customer = $c);
return;
end;
-- 6. Navigation (pages already exist). This statement sets the whole profile,
-- its menu included: list every item the menu must keep.
create or modify navigation Responsive
home page MyModule.Customer_Overview
{
menu item 'Customers' ( OnClick: show page MyModule.Customer_Overview )
};
Related Skills
- Create Page — Full page syntax reference
- Overview Pages — Overview + NewEdit page patterns
- ALTER PAGE/SNIPPET — In-place snippet modification