check-syntax · git:20260906.cbe698a · 2026-09-06 · sha256 34010fc2ac128d15
check-syntax git:20260906.cbe698aA
Immutable. This exact content is served forever at /api/v1/blob/34010fc2ac128d15.
---
name: check-syntax
description: "Validate MDL with `mxcli check` before presenting or executing it, including reference resolution against a project. Use ALWAYS before showing MDL to a user, running `mxcli exec`, or committing a .mdl file — exec refuses exactly what check rejects."
---
# MDL Syntax Validation Skill
This skill ensures MDL scripts are validated before presenting them to users or executing them.
## When to Use This Skill
**ALWAYS** use this skill before:
- Presenting MDL code to users
- Executing MDL scripts via `mxcli exec`
- Committing MDL files to version control
## `exec` refuses what `check` rejects
`mxcli exec` runs the same semantic checks before writing anything. A script whose
checks report an **error** is not executed at all — nothing is written — because
`exec` applies statements one at a time and cannot roll back, so a known-bad
script would leave the model partly updated. Warnings are printed and do not stop
the run.
```bash
mxcli exec script.mdl -p app.mpr # checked, then applied
mxcli exec script.mdl -p app.mpr --no-check # applied regardless
```
This does **not** replace running `check` yourself. `check` is faster, needs no
write connection, and reports the warnings worth reading before you commit to a
run. What the gate guarantees is narrower and still valuable: a script that slips
past you cannot half-apply.
It also does not mean the script is *correct*. `mxcli check` validates MDL syntax
and mxcli's own rules; it does not validate the Mendix model. Run
`mx check` (or `mxcli docker check -p app.mpr`) after applying a slice.
### `-p` resolves references — there is no separate opt-in
`mxcli check script.mdl` alone checks syntax and the semantic rules that need no
model. **Pass `-p` and it also resolves every reference** — modules, entities,
pages, microflows and icons — against that project:
```bash
mxcli check script.mdl # syntax + model-free rules
mxcli check script.mdl -p app.mpr # ... and every reference resolved
```
`--references` is implied by `-p` and is kept only so existing scripts keep
working. It used to be required, which meant `mxcli check script.mdl -p app.mpr`
printed an unqualified `Check passed!` having resolved nothing — a misspelled
icon or entity sailed through a command that had been handed the project. A run
without a project now says what it did not check, so a pass is never read as
more than it is.
### It resolves MEMBER names too, where it can establish the entity
Resolution does not stop at the entity. An attribute named in a **create** or
**change** activity is looked up on that entity *and its generalizations*, so a
typo is reported by `check` rather than by mxbuild as `CE1613 "The selected
attribute '…' no longer exists"` a whole build later:
```
Sales.ACT_Close: Sales.Order has no member "IsArchived" (in change $Order)
— it has OrderNo, Status — mxbuild reports this as CE1613 …
```
This needs the target's entity to be **known**, and that is the boundary worth
understanding rather than assuming:
| the object comes from | checked? |
|---|---|
| a `create Module.Entity (…)` | yes — the entity is in the statement |
| a microflow/nanoflow **parameter** | yes |
| `retrieve $L from Module.Entity` | yes |
| `retrieve $L from $Obj/Module.Assoc` | yes, when `$Obj` is itself typed |
| a `loop` over any of those | yes — the iterator inherits the element type |
| anything else (`send rest request`, `response: file as $Doc`, …) | **no** |
Widget positions are resolved too:
- an **XPath constraint** on a `database from Module.Entity` source — every step
is followed, so a bare name must be an attribute of the entity it lands on and
a `Module.Name` step must be an association or an entity;
- a **template parameter** (`ContentParams` / `CaptionParams`) rooted in a
variable. That one needs **no project** and fires under a bare
`mxcli check`, because the answer is in the statement.
The template-parameter rule is narrower than "no `$` roots", and the difference
is measured rather than reasoned — the writer strips one prefix on one branch:
| `{1} = …` | |
|---|---|
| `OrderNo` | fine |
| `Order_Customer/Name` | fine — association hop, then attribute |
| `$currentObject/Order_Customer/Name` | fine — the prefix is stripped |
| `$currentObject/OrderNo` | **CE1613** |
| `$Order/Name` | **CE1613** |
Note the two-segment form: `Assoc/Attr`, not the XPath `Assoc/Entity/Attr`,
which mxbuild also rejects.
### Expression KINDS are checked in the positions that declare one
Two more things reach mxbuild as `CE0117 "Error(s) in expression"` and are now
reported by `check`:
- **A bare word as a member's value.** Mendix expressions have no bare
identifiers, so `CHANGE $Order (Status = Closed)` is E013. Write `'Closed'`
(a literal), `$Closed` (a variable), or `Module.Enum.Value` (an enumeration).
Scoped to the *whole* value of a create/change member: a bare name **nested**
in a list-operation predicate is legal — `FILTER($L, Status = 'Open')`
resolves `Status` against the item under test — and is not reported.
- **A log message's template parameter must be a String.** `LOG … WITH ({1} =
$Order/Qty)` is E009. Measured on 11.13.0: Integer, Decimal, Boolean,
DateTime and an object each fail; a String attribute is clean; and
`toString(…)` around any of them is clean. So wrap the non-String ones —
the writer is fine, Mendix simply does not coerce here.
### Three more things check now refuses
- **An unqualified CREATE** (`create association Order_Probe …`) — MDL074, no
project needed. `exec` always refused it; check now does too, which matters
because exec is **not transactional**: the statements before the failure are
already applied, and re-running hits "already exists" on them.
- **`RETURNS void AS $x`** — MDL075, no project needed. An alias names the
variable a flow returns, so it cannot be paired with void; mxcli used to
believe the alias and write `return $x` into a flow with no such variable
(CE0109). Write `RETURNS void`, or give the alias the type it holds.
- **`empty($List)`** — E014. `empty` is a Mendix **keyword**, not a function, so
the parser stops at the `(`. Write `$List = empty` or `length($List) = 0`.
**One thing to know about hint ordering**: the reference check runs before
expression checking and **exits on its first error**, so an unrelated mistake
anywhere in a file hides every expression hint in it. If you expect an E0xx and
see none, fix the reference errors first and re-run.
A variable this cannot type is left **unchecked**, never guessed at — a false
"no such member" would block a script that builds cleanly. Two more things are
deliberately not reported: a **qualified** member (`Module.Assoc`), which exec
already refuses when it cannot be an attribute, and any member on an entity the
**script itself** creates or whose attribute the script adds earlier — the
add-the-column-then-populate-it shape stays valid.
## Pre-Flight Validation Checklist
Before writing any MDL, verify these requirements:
### 1. Check Supported Syntax
**Supported in Microflows:**
- `declare $Var type = value;` (primitives only: String/Integer/Long/Decimal/Boolean/DateTime/Enumeration)
- `$entity = create Module.Entity (...);` / `retrieve $entity from ... limit 1;` (objects — **never** `declare` an object; that fails CE0053/CE0038 and is flagged MDL043)
- `$list = create list of Module.Entity;` (lists — **never** `declare` a list; that fails CE0053/CE0038 and is flagged MDL040)
- `set $Var = expression;`
- `$Var = create Module.Entity (attr = value);`
- `change $entity (attr = value);`
- `commit $entity [without events] [refresh];` (omitted = with events, Mendix's default)
- `delete $entity;`
- `retrieve $Var from Module.Entity [where condition];`
- `$Result = call microflow Module.Name (Param = $value);` (NOT `set $Result = ...`)
- `$Result = call nanoflow Module.Name (Param = $value);`
- `show page Module.PageName ($Param = $value);`
- `close page;`
- `validation feedback $entity/attribute message 'message';`
- `log info|warning|error [node 'name'] 'message';`
- `if condition then ... [else ...] end if;`
- `loop $item in $list begin ... end loop;`
- `return $value;`
- `on error continue|rollback|{ handler };`
**Now Supported (previously not):**
- `rollback $entity [refresh];` - Reverts uncommitted changes
- `retrieve ... limit n` - Returns single entity when `limit 1`
- `boolean` without `default` - Auto-defaults to `false`
- `buttonstyle: warning` and `buttonstyle: info` - Now parse correctly
- Keywords as attribute names - `caption`, `label`, `title`, `text`, `content`, `format`, `range`, `source`, `check`, etc. all work unquoted
**NOT Supported (will cause errors):**
- `set $var = call microflow ...` - Use `$var = call microflow ...` (no SET)
- `while ... end while` - Use `loop` with lists
- `case ... when 'String' ...` - Case values are bare enum identifiers, never quoted or qualified; `case ... when Value then ... end case;` itself IS supported (enum splits only), and takes no `else` (MDL008) and no `AS` alias
- `TRY ... CATCH` - Use `on error` blocks
- `break` / `continue` - Not implemented
- `commit message 'text'` - Not in current grammar (session command only)
### 2. Quote All Identifiers
**Best practice: Always quote all identifiers** (entity names, attribute names, parameter names) with double quotes. This escapes every **MDL parser** keyword conflict — quotes are stripped automatically by the parser.
> **Caveat — quoting does not exempt *platform*-reserved member names.** Quoting
> only escapes MDL *parser* keywords. Names the Mendix *platform* reserves for entity
> members are still rejected after the quotes are stripped: `Type` (CE7247, MDL021),
> the system audit attributes `CreatedDate` / `ChangedDate` / `Owner` / `ChangedBy`
> (MDL020 — use the `AutoCreatedDate` / `AutoChangedDate` / `AutoOwner` / `AutoChangedBy`
> pseudo-types instead), plus the CE7247 word list (`ID`, `GUID`, `CurrentUser`, Java
> keywords, …). `"Type": String` still fails MDL021 — rename to a non-reserved name
> (e.g. `ResourceType`, `TypeValue`). "Always safe to quote" covers parser keywords, not
> these.
>
> The parse error tells you which case you are in. `'Title' is a keyword in MDL.
> Quote it to use it as a name` means quoting works and nothing has to be renamed;
> `'Type' is reserved by MENDIX itself, not just by MDL` means it does not.
> Measured across the keywords mxcli hints on: 38 are rescued by quoting, 3
> (`Type`, `Default`, `Owner`) are not.
>
> **Third case — OQL keywords, where quoting works but in a different grammar.**
> `Year`, `Month`, `Quarter`, `Week`, `Day`, `Hour` and the other date-part words are
> neither MDL parser keywords nor platform-reserved: they are accepted everywhere and
> build at 0 errors. They bite only inside a **view entity**'s OQL, and only **unquoted**
> (**CE0174**). OQL takes double-quoted identifiers just like SQL — `s."Month"`,
> `from Module."Year" as s` — and mxcli writes them through unchanged, so the usual fix
> is a quote in the OQL, not a rename. mxcli reports **MDL071** as a *warning* at
> `CREATE`/`ALTER` so the name is still cheap to change if you would rather rename.
> Applies to the **entity** name as well as its attributes.
>
> The exception is an **alias**, and that limit is OQL's own: it takes a bare identifier
> there for any name — `as "Total"`, reserved nowhere, is CE0174 too (**MDL072**). A view
> entity's attribute name is also its select alias, so a view column cannot be called
> `Month` at all — that one needs a rename, not a quote.
>
> **Exception — never quote `$`-prefixed variable/parameter references.** The quote
> rule is for *bare* names (entities, attributes, associations, declared parameter
> names). Variable and parameter **references** in expressions and widget bindings
> stay **unquoted**: `datasource: $X`, `params: { $X: MES."Order" }`, `$currentObject`.
> Quoting them (`"$X"`) breaks resolution ("parameter … references '$X' but no such
> parameter is declared").
>
> **Enumeration values: no `=`.** Value names may be quoted like any identifier, but
> the caption follows as a quoted string — there is **no equals sign**:
> `create enumeration Mod.E ("Grade1" 'Grade 1', Grade2 'Grade 2');` (or `Grade1 caption 'Grade 1'`).
> Writing `"Grade1" = 'Grade 1'` fails with `mismatched input '='` — the `=` is the
> problem, not the quotes.
```sql
create persistent entity Module."Customer" (
"Name": string(200),
"status": string(50),
"create": datetime
);
```
Both `"Name"` and `` `Name` `` syntax are supported. Prefer double quotes for consistency.
Run `mxcli syntax keywords` for the full list of 320+ reserved keywords.
### 3. Validate with mxcli
**Always run these checks:**
```bash
# Step 1: Syntax check (no project needed)
./bin/mxcli check script.mdl
# Step 2: reference validation (needs project)
# Validates microflow bodies, entity/enum references, and widget tree references
# (datasource microflow/nanoflow/entity, action page/microflow, snippet refs)
./bin/mxcli check script.mdl -p app.mpr --references
```
### 4. Common Error Patterns
| Error Message | Likely Cause | Fix |
|---------------|--------------|-----|
| `mismatched input 'set'` after `call microflow` | SET not valid with CALL | Use `$var = call microflow ...` |
| `mismatched input 'create'` | Structural keyword as identifier | Use `"create"` (quoted) or rename |
| `no viable alternative at input` | Unsupported syntax | Check supported statements list |
| `microflow not found` | Referenced before created | Move microflow definition earlier or check spelling |
| `page not found` | Page doesn't exist | Check qualified name with `--references` |
| `entity not found` | Typo or wrong module | Use fully qualified name |
## Two rules that only real validation used to catch
Both are decidable from the MDL alone and now fail `check`, because a project
found them the hard way — four scripts passed `check` with 0 errors, executed
cleanly, and `mx check` then reported them:
| Rule | MxBuild | What it catches |
|---|---|---|
| `MDL-SEC20` | CE0156 | `CREATE USER ROLE` with no **System** module role — nobody holding it can sign in or read System entities. Add `System.User`. **Warning by default, error when the script enables security** (see below). |
| `MDL-PAGE20` | CE5601 | A page with **parameters and a `Url`** where the URL has no segment for a parameter. Mendix binds each parameter from the URL, so the page cannot be opened by link. |
`MDL-SEC20`'s severity follows the security level, because the underlying error
does. Measured on Mendix 11.13: the same role is **CE0156 at security level
Prototype and no error at all at level Off**, where roles are stored but not
validated. A blank project ships `Off`. So the rule warns by default and is an
error only when the script itself contains `ALTER PROJECT SECURITY LEVEL` set to
something other than `Off` — at which point the author has said which world they
are in.
`MDL-PAGE20` accepts an attribute path in the segment (`url: 'p006/{Customer/Name}'`),
which is the usual shape — it matches the segment's leading name, not the whole
segment.
**`check` is still necessary, not sufficient.** Run `mx check` (or
`mxcli docker check`) after every `exec`; these two rules narrow the gap, they do
not close it.
## Validation Workflow
### Before Writing MDL
1. **Read the skill files:**
```bash
cat .claude/skills/write-microflows/SKILL.md
cat .claude/skills/overview-pages/SKILL.md
```
2. **Check help for specific syntax:**
```bash
./bin/mxcli syntax microflow
./bin/mxcli syntax page
./bin/mxcli syntax entity
```
### After Writing MDL
1. **Save to a file:**
```bash
cat > script.mdl << 'EOF'
-- Your MDL here
EOF
```
2. **Run syntax check:**
```bash
./bin/mxcli check script.mdl
```
3. **If errors, check specific syntax:**
```bash
./bin/mxcli syntax keywords # Reserved words
./bin/mxcli syntax microflow # microflow syntax
```
4. **Run reference check (with project):**
```bash
./bin/mxcli check script.mdl -p app.mpr --references
```
5. **Execute only after all checks pass:**
```bash
./bin/mxcli exec script.mdl -p app.mpr
```
## Script Execution Behavior
**IMPORTANT: Script execution is atomic per statement, NOT per script.**
When a script fails on statement N, statements 1 through N-1 have already been committed:
```
Statement 1: create module ✓ (committed)
Statement 2: create entity ✓ (committed)
Statement 3: create association ✓ (committed)
Statement 4: create view entity ✗ (failed - execution stops here)
Statement 5: create page (never executed)
```
**Recommendations:**
1. Split scripts into phases when experimenting with uncertain syntax
2. Use `create or replace` to make scripts idempotent
3. Re-run and check `git status` — a settled script changes nothing
4. Test new syntax patterns with minimal scripts first
5. Keep a backup of your project before running large scripts
## Script Organization
Organize scripts in dependency order:
```mdl
-- check-skip: illustrative ordering example; the PHASE 5 page block uses
-- shorthand pseudo-syntax (layout/title/parameter/widgets) for brevity, not
-- runnable MDL. See create-page for the real page syntax.
-- ============================================
-- PHASE 1: Enumerations (no dependencies)
-- ============================================
create enumeration Module.Status (
Active 'Active',
Inactive 'Inactive'
);
/
-- ============================================
-- PHASE 2: Entities (depend on enumerations)
-- ============================================
create persistent entity Module.Customer (
Name: string(200),
status: Module.Status
);
/
-- ============================================
-- PHASE 3: Associations (depend on entities)
-- ============================================
create association Module.Order_Customer
from Module.Order to Module.Customer
type reference;
/
-- ============================================
-- PHASE 4: Microflows (depend on entities)
-- ============================================
create microflow Module.ACT_Save ($Customer: Module.Customer)
returns boolean as $success
begin
declare $success boolean = false;
commit $Customer;
set $success = true;
return $success;
end;
/
-- ============================================
-- PHASE 5: Pages (depend on microflows)
-- ============================================
create page Module.Customer_Edit
layout Atlas_Default
title 'Edit Customer'
parameter $Customer: Module.Customer
widgets (
-- Can reference microflows created in Phase 4
button 'Save' call microflow Module.ACT_Save (Customer = $Customer)
);
/
```
## Troubleshooting Parse Errors
### Error: "snippet not found" / "page not found"
A reference to a document that hasn't been created yet in the script:
```
Error: snippet not found: MyModule.NavMenu
Error: page not found: MyModule.Customer_NewEdit
```
Script execution is sequential — each `CREATE` commits immediately. Forward references
fail because the target doesn't exist in the database at the moment the referencing
document is created.
**Fix options:**
1. **Reorder** — move the target document's `CREATE` earlier in the script (simplest fix)
2. **Placeholder pattern** — for circular dependencies (e.g. a snippet that shows pages
that embed the snippet), create a minimal placeholder first, then create the referencing
documents, then fill in the placeholder with `CREATE OR MODIFY` — which preserves the
original UUID so all existing bindings remain valid
(see [Resolve Forward References](../resolve-forward-references/SKILL.md))
Declaration order that avoids most forward references:
```
enumerations → entities → snippets (placeholder) → pages → snippets (fill-in) → microflows → navigation
```
> **Never use `CREATE OR REPLACE` for the placeholder fill-in step.** OR REPLACE deletes
> the placeholder and creates a new document with a different UUID, silently breaking
> every page or snippet that references it.
### Error: "mismatched input 'X'"
The word `X` is either:
1. A reserved word - rename the identifier
2. Unsupported syntax - check the supported statements list
3. A typo - check spelling
### Error: "no viable alternative at input"
The parser expected something different:
1. Check for missing semicolons
2. Check for missing `end if`, `end loop`, etc.
3. Verify statement syntax against the reference
### Error: "extraneous input"
Extra tokens found:
1. Check for stray characters
2. Check for duplicate semicolons
3. Verify string quotes are balanced
## Studio Pro MCP — Verification Only
When Studio Pro's embedded MCP server is available **alongside** mxcli (i.e. you are
*not* using mxcli's own `--mcp` backend, but mxcli writes the `.mpr` directly while
Studio Pro is open), use Studio Pro MCP **only for reading and verification**. Never
author with `ped_create_document` / `ped_update_document` — mxcli owns the `.mpr`, and
mixing authoring tools corrupts intent and diverges UUIDs.
### Role split
| Task | Tool |
|------|------|
| Create/modify entities, microflows, pages, nanoflows, nav | mxcli MDL |
| Verify CE errors after exec | `ped_check_errors` |
| Inspect widget tree / microflow body detail | `ped_read_document` |
| Check if a document exists before creating | `ped_find_document` |
| Full model validation | `./mxcli docker check` |
### Studio Pro reads its in-memory model, not the file — a flush IS needed
This is the opposite of what you might expect. mxcli writes directly to the `.mpr`
SQLite file, but **Studio Pro serves `ped_*` reads from its in-memory model**, which
does not hot-reload when an external process changes the file. So after `mxcli exec`:
- `ped_read_document` / `ped_check_errors` will show the **stale** pre-exec model until
Studio Pro re-scans — call `refresh_project` first (or reload the project in the UI).
- **Hazard:** if Studio Pro later saves on its own, it overwrites mxcli's disk write with
its in-memory copy, silently discarding your MDL changes.
**Safest practice:** don't keep the same project open-and-saving in Studio Pro while
mxcli writes it. Either close (or don't save in) Studio Pro during MDL authoring, or
`refresh_project` after every `mxcli exec` before verifying. If you need both writing
*and* a live Studio Pro, use mxcli's `--mcp` backend (which authors *through* Studio
Pro) instead of writing the file directly.
### Step 6: Post-execution verification (add to the workflow above)
After `./mxcli exec script.mdl -p app.mpr` succeeds:
1. `refresh_project` (Studio Pro MCP) so the in-memory model reflects the new file.
2. `ped_check_errors` on each created/modified document for CE errors.
> **Do not treat an empty `DESCRIBE` as proof of a dropped construct.** `DESCRIBE`
> renders from the MDL emitter, which does not yet render every activity/widget type
> (e.g. Java/JavaScript action calls, exclusive splits, some nanoflow buttons). The
> construct may be present in the model even when `DESCRIBE` omits it. To tell a real
> write-drop from an emitter gap, confirm with `ped_read_document` (the live model) or
> `./mxcli docker check` — only flag an engine bug once the live model is also missing it.
## Related Skills
- [/write-microflows](../write-microflows/SKILL.md) - Detailed microflow syntax
- [/overview-pages](../overview-pages/SKILL.md) - Page building syntax
- [/resolve-forward-references](../resolve-forward-references/SKILL.md) - Placeholder pattern and declaration ordering
- [/migrate-oracle-forms](../migrate-oracle-forms/SKILL.md) - Migration-specific guidance