xpath-constraints
XPath constraint syntax for MDL — retrieve WHERE clauses, page data sources, and row-level entity access, including association paths and functions. Use when writing or debugging any XPath in a projec
SKILL.md.claude/skills/mendix/xpath-constraints/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 |
|---|---|
| 28-34 | When to Use This Skill |
| 35-75 | XPath vs Mendix Expressions |
| 76-119 | Syntax Reference |
| 120-292 | How a Constraint Is Laid Out on Disk |
| 293-343 | Usage Contexts |
| 344-383 | Enumeration Attributes |
| 384-425 | Common Patterns |
| 426-437 | Validation |
| 438-447 | Troubleshooting |
XPath Constraints in MDL
This skill provides reference for writing XPath constraint expressions in MDL RETRIEVE statements, page data sources, and security rules.
When to Use This Skill
- Writing
retrieve ... where [xpath]statements in microflows - Writing
database from entity where [xpath]in page data sources - Writing
grant ... where 'xpath'for row-level entity access - Debugging XPath parsing or serialization issues
XPath vs Mendix Expressions
Critical distinction: XPath constraints (inside [...]) use different syntax from Mendix expressions (in SET, IF, DECLARE, etc.):
| Feature | XPath [...] |
Mendix Expression |
|---|---|---|
| Path separator | / (always path traversal) |
/ (also division) |
| Boolean ops | lowercase: and, or, not() |
and, or, not |
| Negation | not(expr) function |
not expr |
| Empty check | = empty, != empty |
= empty |
| Token quoting | '[%CurrentUser%]' (quoted) |
[%CurrentUser%] (unquoted) |
| Nested filter | Assoc/entity[pred] |
Not applicable |
| Arithmetic on value | not supported — pre-compute into a variable | +, -, *, div, mod |
XPath constraints cannot compute values.
where [Seq = $Game/MoveSeq + 1]is a parse error (Mendix XPath has no arithmetic on the value side). Compute the value first, then compare against the variable:declare $Next integer = $Game/MoveSeq + 1; retrieve $M from Mod.Move where [Seq = $Next] first;
mxcli checkexplains this and shows the workaround when it sees+/*/div/modinside a constraint.
A negative literal is fine, though. A leading
-on a number is a value, not arithmetic, and needs no quoting:retrieve $L from Mod.T where [Amount > -7]; retrieve $L from Mod.T where [Amount <= -12.5 and Code != 'X'];
Date arithmetic is not available in XPath.
addDays(),addMonths()and friends are Mendix expression functions — using one in a constraint fails the build withCE0161regardless of its arguments. For relative dates use the date tokens ([%CurrentDateTime%],[%BeginOfCurrentDay%], …), or compute the cut-off in a variable first and compare against that:declare $Cutoff datetime = addDays([%CurrentDateTime%], -7); retrieve $L from Mod.T where [DueDate > $Cutoff];
Syntax Reference
Simple Comparisons
retrieve $Orders from Module.Order
where [State = 'Completed'];
retrieve $Active from Module.Customer
where [IsActive = true];
retrieve $Recent from Module.Order
where [OrderDate != empty];
retrieve $HighValue from Module.Order
where [TotalAmount >= $MinAmount];
Operators: =, !=, <, >, <=, >=
Inline vs quoted form. The inline bracket form
where [State = 'Completed']is preferred. The quoted form —where '[State = ''Completed'']', with internal single quotes doubled ('') — is also accepted forretrieveand datasourcewhereclauses, and now stores the identical constraint (it un-escapes the''and strips the outer quotes). Don't double-bracket: write either[...]or'[...]', not both.
Boolean Logic
-- AND
where [State = 'Completed' and IsPaid = true]
-- OR
where [State = 'Pending' or State = 'Processing']
-- Grouped
where [State = 'Completed' and ($IgnorePaid or IsPaid = true)]
-- NOT
where [not(IsPaid)]
where [not(contains(Name, 'demo'))]
How a Constraint Is Laid Out on Disk
MDL keeps a constraint on one line; how it is stored is decided by mxcli, not by the whitespace you type. A constraint is rebuilt from its parse tree on every write, so there is no original formatting to keep — instead the layout is derived from the expression:
- 80 columns or fewer → stored exactly as written, on one line. This is the common case, and it means adding this changed nothing about existing projects.
- Longer → broken at its top-level
and/orjoints, one clause per line, the operator leading each continuation line. Whereandandormeet, theandruns get explicit parentheses — Mendix bindsandtighter, and a filter is being broken up precisely because it had stopped being obvious at a glance. - Nothing to break on (one long comparison, one long association path) → left whole and over width. Cutting it anywhere else would not be valid XPath.
-- authored (one line, 141 characters)
where [Archived = false and Status = 'Open' and Priority = 'High' and Category = 'Electrical' and Severity > 3 and ReportedOn > '[%BeginOfCurrentDay%]']
-- stored, and what Studio Pro's XPath editor shows
[
Archived = false
and Status = 'Open'
and Priority = 'High'
and Category = 'Electrical'
and Severity > 3
and ReportedOn > '[%BeginOfCurrentDay%]'
]
DESCRIBE puts it back on one line, so a description reads the way it always
has and re-executing it re-derives the same stored text — the unit is reported
Unchanged. A constraint mxcli cannot parse is stored exactly as given rather
than reformatted.
This applies to page datasources, retrieve … where in microflows, and entity
access rules alike.
Association Path Traversal
Bare association paths (without $variable prefix) navigate through the domain model:
-- Single-hop: filter by associated object
where [Module.Order_Customer = $Customer]
-- Multi-hop: traverse through associations
where [Module.Order_Customer/Module.Customer/Name = $CustomerName]
-- Existence check: has an associated object
where [Module.Order_Customer/Module.Customer]
-- Negated existence: has NO associated object
where [not(Module.Order_Customer/Module.Customer)]
Rule: Always use the fully qualified association name (Module.AssociationName).
A bare association name is now caught before the build (MDL-XPATH01).
[Order_Customer = $currentUser]used to passmxcli check --references, get written byexec, and only fail at the build with "Error(s) in XPath constraint" (CE0161) — which is the expensive shape, becauseexeccannot roll back and stops with the model half-updated.checknow names the association and the qualified spelling to use instead. It fires only when the bare name is not an attribute of the constrained entity and is a known association, so attributes stay bare and XPath functions are never touched.
= empty/!= emptydo not work on associations (CE0161 / MDL047).emptytests attribute nullability only. To test whether an object has no associated object, use negated existence:[not(Module.Order_Customer/Module.Customer)]— not[Module.Order_Customer = empty]; to test that it has one, the path itself:[Module.Order_Customer/Module.Customer]— not!= empty.mxcli checkflags both forms as MDL047 before the build does.
Variable Paths
-- Compare attribute via variable path
where [Module.Assoc/Module.Entity/Name = $Variable/Name]
-- Variable on right side
where [Name = $currentObject/SearchString]
-- An association compared with ONE hop off a variable — the hop must land on
-- the same entity as the left side (or a generalization/specialization of it)
where [Module.District_Region = $currentObject/Module.AreaPicker_Region]
What a variable comparand may be (CE0161, measured on 11.13.0 and 11.14.0). At most one hop off the variable (
$currentObject/Mod.Assoc/Nameis MDL055). That one hop compares like with like: an association path with an association landing on the same entity —[Mod.District_Region = $currentObject/Mod.AreaPicker_City](Region vs City) is CE0161 and MDL-XPATH02, as is comparing an association with an attribute ([Mod.District_Region = $currentObject/Label]). The hop is written qualified:$currentObject/AreaPicker_Regionreads as an attribute and is CE0161 / MDL-XPATH01. In a widget data source$currentObjectis the enclosing data container's object; a page parameter ($Picker/…) works the same.
Nested Predicates
Filter intermediate path steps with inline [predicate]:
-- Only lines of completed orders
where [Module.OrderLine_Order/Module.Order[State = 'Completed']]
-- Nested predicate with further traversal
where [Module.OrderLine_Order/Module.Order[State = 'Active']/Module.Order_Category/Module.Category/Name = $CategoryName]
-- reversed() path modifier (traverse association in reverse direction)
where [System.grantableRoles[reversed()]/System.UserRole/System.UserRoles = '[%CurrentUser%]']
Functions
-- String search
where [contains(Name, $SearchStr)]
where [starts-with(Name, $Prefix)]
where [not(contains(Name, 'demo'))]
-- Boolean functions
where [IsActive = true()]
where [Displayed = false()]
Supported functions: contains(), starts-with(), not(), true(), false()
The expression functions startsWith() / endsWith() are not XPath: in a
constraint they are CE0161, and check reports them as MDL091. So is an
operator inside a function argument — starts-with(Name, 'MS-' + $Key) is
CE0161 although Name = 'X-' + $Key is fine: compute the value into a variable
first (declare $P String = 'MS-' + $Key;, then starts-with(Name, $P)). A member the
entity does not have is a reference error in check -p, and so is a system
member written the way describe prints the attribute: XPath spells it
createdDate, changedDate, owner, changedBy — [CreatedDate > …] is CE0161.
Tokens
Mendix tokens provide runtime values. In an XPath constraint a token used as a
value is stored quoted as '[%Token%]' (Studio Pro requires this, or it reports
CE0161). mxcli quotes it for you whether you write the bare or quoted form:
-- Both store identically as '[%CurrentDateTime%]' and pass mx check
where [OrderDate < [%CurrentDateTime%]]
where [OrderDate < '[%CurrentDateTime%]']
where [System.owner = '[%CurrentUser%]']
Tokens are typed.
[%CurrentUser%]is a User reference — compare it only to an association to System.User (e.g.System.owner), never to a String/other attribute ([Title = '[%CurrentUser%]']is a type error → CE0161).[%CurrentDateTime%]compares to DateTime attributes, etc.
System.owner/System.changedBymust be enabled on the entity before you can reference them in XPath, or Studio Pro reports CE0161. Enable withalter entity Module.Entity add attribute owner: autoowner;(mxcli'scheck --referencesflags this). Same forchangedBy/changedDate/createdDate.
Common tokens: [%CurrentUser%], [%CurrentDateTime%], [%CurrentObject%], [%UserRole_RoleName%], [%DayLength%]
ID Pseudo-Attribute
The id pseudo-attribute compares object identity (GUID):
where [id = $currentUser]
where [id != $existingObject]
where [id = '[%CurrentUser%]']
Usage Contexts
RETRIEVE in Microflows
retrieve $Results from Module.Entity
where [IsActive = true and State = 'Ready']
sort by Name asc
limit 100;
The expression inside [...] is parsed as XPath and stored in BSON as the XpathConstraint field.
Page Data Sources
datagrid dg (
datasource: database from Module.Entity where [State != 'Cancelled'] sort by Name asc
) {
column (attribute: Name, caption: 'Name')
}
Multiple bracket constraints can be chained. Consecutive brackets without an operator are treated as AND (standard Mendix XPath):
-- Consecutive brackets (implicit AND) — standard Mendix XPath syntax
datasource: database from Module.Entity where [IsActive = true][Stock > 0]
-- Explicit AND: same result
datasource: database from Module.Entity where [IsActive = true] and [Stock > 0]
-- Mix with OR: combines into single bracket
datasource: database from Module.Entity where [IsActive = true] or [Stock > 10]
GRANT Entity Access (Security)
Security rules take the XPath in brackets, like every other XPath, so quotes inside it are written once:
mdl 1;
grant read *, write * on entity Module.Entity to Module.Role
where [System.owner = '[%CurrentUser%]'];
Sibling groups (where [a][b]) are kept as one constraint. The old quoted form
grant Module.Role on Module.Entity (...) where '[...]' still parses, warns
MDL-DEPR030, and mxcli fmt --upgrade rewrites it.
Enumeration Attributes
Critical: XPath constraints are translated to database SQL WHERE clauses at runtime. The database stores enum values as plain strings (the value key), not qualified names. This means:
[Status = 'Open']— always valid: direct string literal match[Status = Module.OrderStatus.Open]— also valid: mxcli converts to'Open'in BSON automatically
Both forms are accepted by mxcli in the write direction. DESCRIBE MICROFLOW always shows the qualified name form for readability, even though BSON stores 'Open'.
Do NOT use qualified names in expression context (IF, SET, DECLARE) for comparisons — those contexts use a different form. See write-microflows "Enumeration Comparisons" section.
-- Preferred (mxcli converts to 'Open' in BSON):
retrieve $OpenOrders from Module.Order
where [Status = Module.OrderStatus.Open];
-- Also accepted (stored as-is):
retrieve $OpenOrders from Module.Order
where [Status = 'Open'];
-- NOT equal
retrieve $Active from Module.Order
where [Status != Module.OrderStatus.Cancelled];
-- OR across multiple enum values
retrieve $InProgress from Module.Order
where [Status = Module.OrderStatus.Open or Status = Module.OrderStatus.Processing];
-- Enum combined with other predicates
retrieve $Results from Module.Order
where [Status = Module.OrderStatus.Completed and TotalAmount >= $MinAmount];
Troubleshooting silent empty results with enums
If a RETRIEVE returns empty unexpectedly when filtering by an enum attribute:
- Check the value key (not caption) — the key is what's stored in the DB column. Check with
DESCRIBE ENUMERATION Module.EnumName. - Keys are case-sensitive:
'open'≠'Open'. - Confirm the attribute type is actually an enumeration and not a string —
DESCRIBE ENTITY Module.EntityName.
Common Patterns
Parameterized Search
mdl 1;
create microflow Module.Search ($query: string, $ActiveOnly: boolean)
returns boolean
begin
retrieve $Results from Module.Customer
where [($ActiveOnly = false or IsActive = true)
and (contains(Name, $query) or contains(Email, $query))];
return true;
end;
Date Range Filter
retrieve $Orders from Module.Order
where [OrderDate >= $StartDate and OrderDate <= $EndDate];
Optional Filters (empty = skip)
retrieve $Orders from Module.Order
where [($Category = empty or Module.Order_Category = $Category)
and ($State = empty or State = $State)];
Owner-Based Security
-- In microflow
retrieve $MyItems from Module.Item
where [System.owner = '[%CurrentUser%]'];
-- In security rule
grant read * on entity Module.Item to Module.User where [System.owner = '[%CurrentUser%]'];
Validation
Always validate XPath syntax before execution:
# Syntax check (no project needed)
./bin/mxcli check script.mdl
# with reference validation (needs project)
./bin/mxcli check script.mdl -p app.mpr --references
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
mismatched input on keyword |
Attribute name is a reserved word | This is handled — xpathWord accepts any keyword as identifier |
| Token not quoted in BSON | Token in Mendix expression context | Use [...] bracket syntax for XPath, not bare expression |
CE0111 path error |
Missing module prefix on association | Use Module.AssociationName, not just AssociationName |
CE0161 XPath constraint error |
Qualified name used for non-enum or wrong format | Use string literal 'Value' or qualified name Module.Enum.Value; mxcli converts automatically |
not parsed as keyword |
Using not (uppercase) in XPath |
XPath uses lowercase not() as a function |
| Retrieve returns empty for enum filter | String literal value key mismatch | Key is case-sensitive; verify with DESCRIBE ENUMERATION Module.Name |