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 project.
Scanned 10/4/2026
npx -y skills add mendixlabs/mxcli --skill xpath-constraints --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Xpath Constraints?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mendixlabs-xpath-constraints)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: xpath-constraints
description: "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 project."
---
# 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:
> ```mdl
> declare $Next integer = $Game/MoveSeq + 1;
> retrieve $M from Mod.Move where [Seq = $Next] first;
> ```
> `mxcli check` explains this and shows the workaround when it sees `+`/`*`/`div`/
> `mod` inside a constraint.
> **A negative literal is fine, though.** A leading `-` on a number is a value,
> not arithmetic, and needs no quoting:
> ```mdl
> 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 with `CE0161` regardless 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:
> ```mdl
> declare $Cutoff datetime = addDays([%CurrentDateTime%], -7);
> retrieve $L from Mod.T where [DueDate > $Cutoff];
> ```
## Syntax Reference
### Simple Comparisons
```mdl
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 for `retrieve` and datasource
> `where` clauses, 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
```mdl
-- 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`/`or` joints, one clause per line,
the operator leading each continuation line. Where `and` and `or` meet, the
`and` runs get explicit parentheses — Mendix binds `and` tighter, 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:
```mdl
-- 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 pass `mxcli check --references`, get
> written by `exec`, and only fail at the build with *"Error(s) in XPath
> constraint"* (**CE0161**) — which is the expensive shape, because `exec` cannot
> roll back and stops with the model half-updated. `check` now 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` / `!= empty` do not work on associations (CE0161 / MDL047).**
> `empty` tests *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 check` flags both forms as **MDL047** before the build does.
### Variable Paths
```mdl
-- Compare attribute via variable path
where [Module.Assoc/Module.Entity/Name = $Variable/Name]
-- Variable on right side
where [Name = $currentObject/SearchString]
```
### Nested Predicates
Filter intermediate path steps with inline `[predicate]`:
```mdl
-- 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
```mdl
-- 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**. 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:
```mdl
-- 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.changedBy` must be enabled** on the entity before you
> can reference them in XPath, or Studio Pro reports CE0161. Enable with
> `alter entity Module.Entity add attribute owner: autoowner;` (mxcli's
> `check --references` flags this). Same for `changedBy`/`changedDate`/`createdDate`.
Common tokens: `[%CurrentUser%]`, `[%CurrentDateTime%]`, `[%CurrentObject%]`, `[%UserRole_RoleName%]`, `[%DayLength%]`
### ID Pseudo-Attribute
The `id` pseudo-attribute compares object identity (GUID):
```mdl
where [id = $currentUser]
where [id != $existingObject]
where [id = '[%CurrentUser%]']
```
## Usage Contexts
### RETRIEVE in Microflows
```mdl
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
```mdl
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):
```mdl
-- 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
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.
```mdl
-- 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:
1. Check the **value key** (not caption) — the key is what's stored in the DB column. Check with `DESCRIBE ENUMERATION Module.EnumName`.
2. Keys are **case-sensitive**: `'open'` ≠ `'Open'`.
3. Confirm the attribute type is actually an enumeration and not a string — `DESCRIBE ENTITY Module.EntityName`.
## Common Patterns
### Parameterized Search
```mdl
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
```mdl
retrieve $Orders from Module.Order
where [OrderDate >= $StartDate and OrderDate <= $EndDate];
```
### Optional Filters (empty = skip)
```mdl
retrieve $Orders from Module.Order
where [($Category = empty or Module.Order_Category = $Category)
and ($State = empty or State = $State)];
```
### Owner-Based Security
```mdl
-- 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:
```bash
# 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` |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!