resolve-forward-references · git:20261013.b006788 · 2026-10-13 · sha256 45e5780c208494b9
resolve-forward-references git:20261013.b006788A
Immutable. This exact content never changes and is served at /api/v1/blob/45e5780c208494b9.
---
name: resolve-forward-references
description: "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 something defined later in the same file."
---
<!-- 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 |
|---|---|
| 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 |
<!-- /skill-index -->
# 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 PAGE` inside 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:
1. **Create a minimal placeholder** for the document that will be referenced.
2. **Create all documents that reference it.** They bind to the placeholder's ID.
3. **Fill in the placeholder** using `CREATE OR MODIFY` or `ALTER` — both preserve the
original ID so existing bindings remain valid.
> **Write `CREATE OR MODIFY`, not `CREATE OR REPLACE`.** `OR REPLACE` is the deprecated
> spelling of `OR MODIFY` (`MDL-DEPR001`, refused from `mdl 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 one `OR REPLACE` that does delete and recreate is `create or replace view
> entity` in a script without an `mdl 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.
```sql
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.
```sql
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
```sql
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 |
```sql
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
```sql
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](../create-page/SKILL.md) — Full page syntax reference
- [Overview Pages](../overview-pages/SKILL.md) — Overview + NewEdit page patterns
- [ALTER PAGE/SNIPPET](../alter-page/SKILL.md) — In-place snippet modification