Preface
This documentation covers mxcli, a command-line tool for reading, querying, and modifying Mendix application projects, and MDL (Mendix Definition Language), the SQL-like language it uses. The material is organized into eight parts:
- Part I: Tutorial walks you through installation, exploring a project, making your first changes, and integrating with AI assistants.
- Part II: The MDL Language is a comprehensive guide to MDL syntax – domain models, microflows, pages, security, navigation, workflows, and business events.
- Part III: Project Tools covers code navigation, catalog queries, linting, testing, external SQL, and Docker integration.
- Part IV: IDE Integration describes the VS Code extension, LSP server, and
mxcli initproject setup. - Part V: Go Library documents the Go API for programmatic access to Mendix projects.
- Part VI: MDL Statement Reference provides detailed syntax and examples for every MDL statement.
- Part VII: Architecture and Internals explains the MPR file format, parser pipeline, and catalog system for contributors.
- Part VIII: Appendixes includes quick reference tables, reserved words, error messages, and a glossary.
You can read the Tutorial sequentially or jump directly to the section relevant to your task. The Statement Reference (Part VI) is designed for lookup rather than linear reading.
What is mxcli?
mxcli is a command-line tool that enables developers and AI coding assistants to read, understand, and modify Mendix application projects. Mendix projects are stored in binary .mpr files that cannot be read or edited as text. mxcli bridges this gap by providing a text-based interface using MDL (Mendix Definition Language), a SQL-like syntax for querying and manipulating Mendix models.
How It Works
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Developer │ │ mxcli │ │ Mendix Project │
│ or AI Agent │────>│ (MDL REPL) │────>│ (.mpr file) │
│ │ MDL │ │ │ │
│ "Create a │ │ Parses MDL │ │ Creates actual │
│ Customer │ │ Validates │ │ entities, │
│ entity..." │ │ Executes │ │ microflows, │
└─────────────────┘ └──────────────┘ │ pages, etc. │
└─────────────────┘
A developer or AI agent writes MDL statements. mxcli parses and validates them, then applies the changes directly to the .mpr project file. The modified project can then be opened in Mendix Studio Pro as usual.
Important: Do not edit a project with mxcli while it is open in Studio Pro. Studio Pro maintains in-memory caches that cannot be updated externally, and concurrent edits will cause errors. Close the project in Studio Pro first, run mxcli, then re-open the project.
Key Capabilities
- Project exploration – list modules, entities, microflows, pages; describe any element in MDL; full-text search across all strings and source definitions.
- Code navigation – find callers, callees, references, and impact analysis for any project element.
- Model modification – create and alter entities, microflows, pages, security rules, navigation, workflows, and more using MDL scripts.
- Catalog queries – SQL-based querying of project metadata (entity counts, microflow complexity, widget usage, cross-references).
- Linting and reports – 40+ built-in lint rules with SARIF output for CI; scored best-practices reports.
- Testing – test microflows using MDL syntax with javadoc-style annotations.
- AI assistant integration – works with Claude Code, Cursor, Continue.dev, Windsurf, and Aider.
mxcli initsets up project configuration, skills, and a Dev Container for sandboxed AI development. - VS Code extension – syntax highlighting, diagnostics, code completion, hover, go-to-definition, and context menu commands for
.mdlfiles. - External SQL – connect to PostgreSQL, Oracle, or SQL Server; query external databases; import data into a running Mendix application.
Supported Environments
| Item | Details |
|---|---|
| Mendix versions | Studio Pro 8.x, 9.x, 10.x, 11.x |
| MPR formats | v1 (single .mpr file) and v2 (.mpr + mprcontents/ folder) |
| Platforms | Linux, macOS, Windows (amd64 and arm64) |
| Dependencies | None – mxcli is a single static binary with no runtime dependencies |
What is MDL?
MDL (Mendix Definition Language) is a text-based, SQL-like language for describing and manipulating Mendix application models. It provides a human-readable representation of Mendix project elements – entities, microflows, pages, security rules, and more – at the same abstraction level as the visual models in Studio Pro.
Why a Text-Based Language?
Mendix projects are stored in a binary format (.mpr files containing BSON documents in SQLite). While Studio Pro provides a visual interface, there is no standard text format for Mendix models. This creates problems for:
- AI assistants that need to read and generate model elements
- Code review of changes to Mendix projects
- Scripting and automation of repetitive modeling tasks
- Documentation of project structure and design patterns
MDL addresses these problems by providing a syntax that is both human-readable and machine-parseable.
Characteristics of MDL
- SQL-like syntax – uses familiar keywords like
CREATE,ALTER,DROP,SHOW,DESCRIBE,GRANT. - Declarative – you describe what the model should look like, not how to construct it step by step.
- Complete – covers domain models, microflows, pages, security, navigation, workflows, and business events.
- Validated – mxcli can check MDL scripts for syntax errors and reference validity before applying them.
Token Efficiency
When working with AI coding assistants, context window size is a critical constraint. MDL’s compact syntax uses significantly fewer tokens than equivalent JSON model representations:
| Representation | Tokens for a 10-entity module |
|---|---|
| JSON (raw model) | ~15,000–25,000 tokens |
| MDL | ~2,000–4,000 tokens |
| Reduction | 5–10x fewer tokens |
Fewer tokens means lower API costs, more application context fits in a single prompt, and AI assistants produce more accurate output because there is less noise in their input.
Example: JSON vs MDL
A simple entity definition illustrates the difference.
JSON representation (~180 tokens):
{
"entity": {
"name": "Customer",
"documentation": "Customer master data",
"attributes": [
{"name": "Name", "type": {"type": "String", "length": 200}},
{"name": "Email", "type": {"type": "String", "length": 200}}
]
}
}
MDL representation (~35 tokens):
/** Customer master data */
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200),
Email: String(200)
);
The MDL version conveys the same information in roughly one-fifth the space, and its structure is immediately recognizable to anyone familiar with SQL DDL.
What MDL Covers
MDL provides statements for the following areas of a Mendix project:
| Area | Example Statements |
|---|---|
| Domain model | CREATE ENTITY, ALTER ENTITY, CREATE ASSOCIATION |
| Microflows | CREATE MICROFLOW, CREATE NANOFLOW |
| Pages | CREATE PAGE, CREATE SNIPPET, ALTER PAGE |
| Security | CREATE MODULE ROLE, GRANT, REVOKE |
| Navigation | ALTER NAVIGATION |
| Workflows | CREATE WORKFLOW |
| Business events | CREATE BUSINESS EVENT SERVICE |
| Project queries | SHOW MODULES, DESCRIBE ENTITY, SEARCH |
| Catalog | REFRESH CATALOG, SELECT FROM CATALOG |
See Part II (The MDL Language) for a complete guide, or Part VI (MDL Statement Reference) for detailed syntax of each statement.
Mendix Concepts for Newcomers
This page explains the core Mendix concepts you’ll encounter when using mxcli. If you’re familiar with web frameworks, relational databases, or backend development, most of these will map to things you already know.
The Big Picture
A Mendix application is a model-driven app. Instead of writing code in files, developers build applications visually in Studio Pro (the Mendix IDE). The entire application model – data structures, logic, UI, security – is stored in a single binary file called an MPR (Mendix Project Resource).
mxcli lets you read and modify that MPR file using text commands, without opening Studio Pro.
Modules
A module is the top-level organizational unit, like a package in Go or a namespace in C#. Each module has its own:
- Domain model (data structures)
- Microflows and nanoflows (logic)
- Pages (UI)
- Enumerations (constant sets)
- Security settings
A typical project has a few custom modules (Sales, Admin, Integration) plus system modules provided by the platform (System, Administration).
SHOW MODULES;
Domain Model
The domain model is the data layer of a module. If you know relational databases, the mapping is straightforward:
| Mendix Concept | Relational Equivalent | MDL Syntax |
|---|---|---|
| Entity | Table | CREATE ENTITY |
| Attribute | Column | Inside entity definition |
| Association | Foreign key / Join table | CREATE ASSOCIATION |
| Generalization | Table inheritance | EXTENDS |
| Enumeration | Enum / Check constraint | CREATE ENUMERATION |
Entities
An entity defines a data type. There are several kinds:
- Persistent – stored in the database (the default and most common)
- Non-persistent – exists only in memory during a user session, useful for form state or temporary calculations
- View – backed by an OQL query, like a database view
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200) NOT NULL,
Email: String(200),
IsActive: Boolean DEFAULT true
);
Associations
An association is a relationship between two entities. Think of it as a foreign key.
- Reference – many-to-one or one-to-one (a foreign key column on the “from” entity)
- ReferenceSet – many-to-many (a join table under the hood)
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Order TO Sales.Customer
TYPE Reference;
Generalization
Entities can inherit from other entities using generalization (the EXTENDS keyword). The child entity gets all parent attributes. This is commonly used with system entities like System.Image (for file uploads) or System.User (for user accounts).
CREATE PERSISTENT ENTITY Sales.ProductImage EXTENDS System.Image (
Caption: String(200)
);
Microflows and Nanoflows
Microflows are the server-side logic of a Mendix app. They’re visual flowcharts in Studio Pro, but in MDL they read like imperative code with activities:
- Retrieve data from the database
- Create, Change, Commit, Delete objects
- Call other microflows or external services
- If/Else branching, Loop iteration
- Show Page, Log Message, Validation Feedback
CREATE MICROFLOW Sales.CreateOrder(
DECLARE $Customer: Sales.Customer
)
RETURN Boolean
BEGIN
CREATE $Order: Sales.Order (
OrderDate = [%CurrentDateTime%],
Status = 'Draft'
);
CHANGE $Order (
Sales.Order_Customer = $Customer
);
COMMIT $Order;
RETURN true;
END;
Nanoflows are the client-side equivalent. They run in the browser (or native mobile app) and are useful for offline-capable logic and low-latency UI interactions. They have the same syntax but fewer available activities (no database transactions, no direct service calls).
Pages
Pages define the user interface. A page has:
- A layout – the outer frame (header, sidebar, footer) shared across pages
- Widgets – the UI components inside the page
Widgets are nested in a tree. Common widget types:
| Widget | Purpose | Analogy |
|---|---|---|
| DataView | Displays one object | A form |
| DataGrid | Displays a list as a table | An HTML table with sorting/search |
| ListView | Displays a list with custom layout | A repeating template |
| TextBox | Text input bound to an attribute | An <input> field |
| Button | Triggers an action | A <button> |
| Container | Groups other widgets | A <div> |
| LayoutGrid | Responsive column layout | CSS grid / Bootstrap row |
CREATE PAGE Sales.CustomerOverview
LAYOUT Atlas_Core.Atlas_Default
TITLE 'Customers'
(
DATAGRID SOURCE DATABASE Sales.Customer (
COLUMN Name,
COLUMN Email,
COLUMN IsActive
)
);
Snippets
A snippet is a reusable page fragment. You define it once and embed it in multiple pages using a SnippetCall. Think of it as a component or partial template.
Security
Mendix uses a role-based access control model:
- Module roles are defined per module (e.g.,
Sales.Admin,Sales.User) - User roles are defined at the project level and aggregate module roles
- Access rules control what each module role can do with entities (CREATE, READ, WRITE, DELETE) and which microflows/pages they can access
CREATE MODULE ROLE Sales.Manager;
GRANT CREATE, READ, WRITE ON Sales.Customer TO Sales.Manager;
GRANT EXECUTE ON MICROFLOW Sales.CreateOrder TO Sales.Manager;
GRANT VIEW ON PAGE Sales.CustomerOverview TO Sales.Manager;
Navigation
Navigation profiles define how users move through the app. There are profiles for responsive web, tablet, phone, and native mobile. Each profile has:
- A home page (the landing page after login)
- A menu with items that link to pages or microflows
Workflows
Workflows model long-running processes with human tasks. Think of them as state machines for approval flows, onboarding processes, or multi-step procedures. A workflow has:
- User tasks – steps that require human action
- Decisions – branching based on conditions
- Parallel splits – concurrent paths
Workflows complement microflows: microflows handle immediate logic, workflows handle processes that span hours or days.
How It All Fits Together
Project
├── Module: Sales
│ ├── Domain Model
│ │ ├── Entity: Customer (Name, Email, IsActive)
│ │ ├── Entity: Order (OrderDate, Status)
│ │ └── Association: Order_Customer
│ ├── Microflows
│ │ ├── CreateOrder
│ │ └── ApproveOrder
│ ├── Pages
│ │ ├── CustomerOverview
│ │ └── OrderEdit
│ ├── Enumerations
│ │ └── OrderStatus (Draft, Active, Closed)
│ └── Security
│ ├── Module Role: Manager
│ └── Module Role: Viewer
├── Module: Administration
│ └── ...
└── Navigation
└── Responsive profile → Home: Sales.CustomerOverview
In mxcli, you can explore this structure with:
SHOW STRUCTURE DEPTH 2;
What’s Next
- Part I: Tutorial – hands-on walkthrough
- Part II: The MDL Language – complete language guide
- Glossary – alphabetical reference of all terms
Document Conventions
This page describes the formatting and notation conventions used throughout this documentation.
Code Examples
MDL examples use sql code fencing because MDL’s syntax is SQL-like and this provides appropriate syntax highlighting:
CREATE PERSISTENT ENTITY MyModule.Customer (
Name: String(200) NOT NULL
);
Go code examples use go code fencing:
reader, err := modelsdk.Open("/path/to/app.mpr")
defer reader.Close()
Shell commands use bash code fencing:
mxcli -p app.mpr -c "SHOW MODULES"
Syntax Notation
When describing the syntax of MDL statements, the following conventions apply:
| Notation | Meaning |
|---|---|
UPPERCASE | Keywords – type them exactly as shown |
lowercase | User-provided values (names, expressions, types) |
[brackets] | Optional clause – may be omitted |
... | Repetition – the preceding element may appear multiple times |
| `a | b` |
( ) | Grouping – used to clarify precedence in syntax descriptions |
For example, the notation:
CREATE [PERSISTENT] ENTITY module.name (
attribute_name: type [NOT NULL] [, ...]
);
means: CREATE and ENTITY are required keywords; PERSISTENT is optional; module.name is a user-provided qualified name; each attribute has a name and type, with an optional NOT NULL constraint; and additional attributes may follow, separated by commas.
Cross-References
References to MDL statements link to their detailed pages in Part VI (MDL Statement Reference) using the format “See CREATE ENTITY” or “See GRANT”. References to conceptual explanations link to the relevant section in Part II (The MDL Language).
Terminology
Mendix-specific terms such as “entity”, “microflow”, “nanoflow”, “module”, and “domain model” follow standard Mendix terminology. See the Glossary for definitions.
Setting Up
Before you can start exploring and modifying Mendix projects from the command line, you need three things:
- mxcli installed on your machine
- A Mendix project (an
.mprfile) to work with - A feel for the REPL, the interactive shell where you’ll spend most of your time
This chapter walks you through all three.
Installation methods
There are four ways to get started with mxcli:
- Playground – open the mxcli Playground in a GitHub Codespace. Zero install, runs in your browser with a sample Mendix project, tutorials, and example scripts. Best way to try mxcli for the first time.
- Binary download – grab a pre-built binary from GitHub Releases. Quickest path if you want to use mxcli on your own project.
- Build from source – clone the repo and run
make build. Useful if you want the latest unreleased changes or plan to contribute. - Dev Container (recommended for your own projects) – run
mxcli initon your Mendix project, open it in VS Code, and reopen in the container. This gives you mxcli, a JDK, Docker-in-Docker, and Claude Code all pre-configured in a sandboxed environment. This is the recommended approach, especially when pairing with AI coding assistants.
The next few pages cover each method, then walk you through opening a project and using the REPL.
Alpha software warning. mxcli can corrupt your Mendix project files. Always work on a copy of your
.mpror use version control (Git) before making changes.
Installation
Pick whichever method suits your situation. If you just want to try mxcli without installing anything, start with the Playground. If you’re planning to use mxcli on your own project with an AI coding assistant, skip to the Dev Container section.
Playground (zero install)
The fastest way to try mxcli. The mxcli Playground is a GitHub repository with a pre-configured Mendix project, example scripts, and tutorials. Open it in a Codespace and start using mxcli immediately – nothing to install on your machine.
The Codespace comes with mxcli, a JDK, Docker-in-Docker, Claude Code, and a sample Mendix 11.x project ready to explore and modify. It includes:
- 5 example scripts – explore, create entities, microflows, pages, and security
- Step-by-step tutorials – from first steps through linting and testing
- AI tool configs – pre-configured for Claude Code, Cursor, Windsurf, Continue.dev, and Aider
Once the Codespace is running:
./mxcli -p App.mpr -c "SHOW STRUCTURE" # Explore the project
./mxcli exec scripts/01-explore.mdl -p App.mpr # Run an example script
./mxcli # Start interactive REPL
When you’re ready to work on your own Mendix project, use one of the installation methods below.
Binary download
Pre-built binaries are available for Linux, macOS, and Windows on both amd64 and arm64 architectures.
- Go to the GitHub Releases page.
- Download the archive for your platform (e.g.,
mxcli_linux_amd64.tar.gzormxcli_darwin_arm64.tar.gz). - Extract the binary and move it somewhere on your
PATH:
# Example for Linux/macOS
tar xzf mxcli_linux_amd64.tar.gz
sudo mv mxcli /usr/local/bin/
On Windows, extract the .zip and add the folder containing mxcli.exe to your system PATH.
Build from source
Building from source requires Go 1.24 or later and Make. No C compiler is needed – mxcli uses a pure-Go SQLite driver.
git clone https://github.com/mendixlabs/mxcli.git
cd mxcli
make build
The binary lands at ./bin/mxcli. You can copy it to a directory on your PATH or run it directly:
./bin/mxcli --version
Dev Container (recommended)
The Dev Container approach is the recommended way to work with mxcli, particularly when using AI coding assistants. It creates a sandboxed environment so that agents can only access your project files, not the rest of your system.
Here’s how to set it up:
Step 1: Install mxcli using one of the methods above (you need it locally to run init).
Step 2: Run mxcli init on your Mendix project:
mxcli init /path/to/my-mendix-project
This creates a .devcontainer/ folder (along with skill files, agent configs, and other goodies) inside your project directory.
Step 3: Open the project folder in VS Code and click “Reopen in Container” when prompted (or use the Command Palette: Dev Containers: Reopen in Container).
VS Code will build and start the container. This takes a minute or two the first time.
What’s inside the Dev Container
The container comes with everything you need pre-installed:
| Component | What it’s for |
|---|---|
| mxcli | The CLI itself, copied into the project |
| JDK 21 (Adoptium) | Required by MxBuild for project validation |
| Docker-in-Docker | Running Mendix apps locally with mxcli docker run |
| Node.js | Playwright testing support |
| PostgreSQL client | Database connectivity for demo data |
| Claude Code | AI coding assistant (auto-installed on container creation) |
Once the container is running, mxcli is ready to use – no further setup needed.
Specifying AI tools
By default, mxcli init configures for Claude Code. You can target other tools too:
# Cursor only
mxcli init --tool cursor /path/to/my-mendix-project
# Multiple tools
mxcli init --tool claude --tool cursor /path/to/my-mendix-project
# Everything
mxcli init --all-tools /path/to/my-mendix-project
Run mxcli init --list-tools to see all supported tools.
Verify your installation
Whichever method you used, confirm that mxcli is working:
mxcli --version
You should see version and build information printed to the terminal. If you get a “command not found” error, double-check that the binary is on your PATH.
You’re ready to open a project.
Opening Your First Project
mxcli works with Mendix project files – the .mpr files that Mendix Studio Pro creates. Let’s open one.
Back up first. mxcli is alpha software and can corrupt project files. Before you start, either make a copy of your
.mprfile or make sure the project is under version control (Git). This isn’t just a disclaimer – take it seriously.
The -p flag
The most common way to point mxcli at a project is with the -p flag:
mxcli -p /path/to/app.mpr -c "SHOW MODULES"
This opens the project in read-only mode, runs the command, and exits. The -p flag works with all mxcli subcommands.
Opening in the REPL
You can also open a project from inside the interactive REPL:
# Start the REPL, then open a project
mxcli
OPEN PROJECT '/path/to/app.mpr';
SHOW MODULES;
Or pass -p when launching the REPL so it opens the project immediately:
mxcli -p /path/to/app.mpr
-- Project is already open, start working
SHOW MODULES;
When you provide -p, the project stays open for the duration of your REPL session. You don’t need to pass it again for each command.
Read-only vs read-write
By default, mxcli opens projects in read-only mode. This is safe for exploration – you can browse modules, describe entities, search through the project, and run catalog queries without risk.
When you execute a command that modifies the project (like CREATE ENTITY), mxcli automatically upgrades to read-write mode. You’ll see a confirmation message when this happens.
MPR format auto-detection
Mendix projects come in two formats:
- v1: A single
.mprSQLite database file (Mendix versions before 10.18) - v2: An
.mprmetadata file plus anmprcontents/folder with individual documents (Mendix 10.18 and later)
You don’t need to worry about which format your project uses. mxcli detects the format automatically and handles both transparently. Just point it at the .mpr file either way.
Working in a Dev Container
If you’re using the Dev Container setup from the previous page, your project is already mounted in the container. The typical path is:
mxcli -p app.mpr
The Dev Container sets the working directory to your project root, so relative paths work naturally.
Quick sanity check
Once you have a project open, try listing the modules:
mxcli -p app.mpr -c "SHOW MODULES"
You should see a table of module names. If you see your project’s modules listed, everything is working and you’re ready to explore.
Next up: the REPL, where the real fun starts.
The REPL
The mxcli REPL is an interactive shell for working with Mendix projects, much like psql is for PostgreSQL or mysql for MySQL. You type MDL statements, press Enter, and see results immediately.
Starting the REPL
Launch it with or without a project:
# Start with a project already loaded
mxcli -p app.mpr
# Start without a project (you can open one later)
mxcli
You’ll see a prompt where you can start typing MDL:
mxcli>
Running commands
Type any MDL statement and press Enter:
SHOW MODULES;
Results are printed as formatted tables directly in the terminal.
Multi-line statements
MDL statements end with a semicolon (;). If you press Enter before typing a semicolon, mxcli knows you’re still writing and waits for more input:
CREATE ENTITY MyModule.Customer (
Name: String(200) NOT NULL,
Email: String(200),
IsActive: Boolean DEFAULT true
);
The REPL shows a continuation prompt (.....>) while you’re in a multi-line statement. The statement executes when you type the closing ; and press Enter.
Getting help
Type HELP to see a list of available commands:
HELP;
This prints a categorized overview of all MDL statements – useful when you can’t remember the exact syntax for something.
Command history
Use the up and down arrow keys to scroll through previous commands, just like in any other shell. This is especially handy when you’re iterating on a query or tweaking a CREATE statement.
Exiting
When you’re done, type either:
EXIT;
or:
QUIT;
You can also press Ctrl+D to exit.
One-off commands with -c
You don’t always need an interactive session. The -c flag lets you run a single command and exit:
mxcli -p app.mpr -c "SHOW ENTITIES IN MyModule"
This is great for quick lookups and for scripting. The output is pipe-friendly, so you can chain it with other tools:
# Count entities per module
mxcli -p app.mpr -c "SHOW MODULES" | tail -n +2 | while read module; do
echo "$module: $(mxcli -p app.mpr -c "SHOW ENTITIES IN $module" | wc -l) entities"
done
Running script files
For anything beyond a quick one-liner, you can put MDL statements in a .mdl file and execute the whole thing:
mxcli -p app.mpr -c "EXECUTE SCRIPT 'setup.mdl'"
Or from the REPL:
EXECUTE SCRIPT 'setup.mdl';
This is how you’ll typically apply larger changes – write the MDL in a file, check the syntax with mxcli check, then execute it.
The TUI (Terminal UI)
If you prefer a more visual experience, mxcli also offers a graphical terminal interface:
mxcli tui -p app.mpr
The TUI gives you a split-pane layout with a project tree, an MDL editor, and output panels. It’s built on top of the same REPL engine, so all the same commands work. This can be a nice middle ground between the bare REPL and opening Studio Pro.
What’s next
Now that you know how to install mxcli, open a project, and use the REPL, you’re ready to start exploring. The next chapter walks through the commands you’ll use most often: SHOW, DESCRIBE, and SEARCH.
Exploring a Project
Once you have a project open – whether through the REPL, a CLI one-liner, or a script file – the next step is to look around. What modules exist? What entities are defined? What does a particular microflow do?
mxcli provides three families of commands for exploration:
- SHOW commands list elements by type.
SHOW ENTITIESlists all entities;SHOW MICROFLOWS IN Salesnarrows the list to one module. - DESCRIBE commands display the full MDL source for a single element, giving you the complete definition including attributes, associations, logic, and widget trees.
- SEARCH performs full-text search across every string in the project – captions, messages, expressions, documentation, and more.
- SHOW STRUCTURE gives you a compact tree view of the entire project or a single module, at varying levels of detail.
These commands are read-only. They never modify your project. You can run them freely to build a mental model of the application before making any changes.
What you will learn
In this chapter, you will:
- List modules, entities, microflows, pages, and other elements with SHOW commands
- Inspect the full definition of any element with DESCRIBE
- Find elements by keyword with SEARCH
- Get a bird’s-eye view of project structure with SHOW STRUCTURE
Prerequisites
You should have mxcli installed and know how to open a project. If not, work through the Setting Up chapter first.
The examples in this chapter assume you have a Mendix project open. You can follow along using either the REPL or CLI one-liners:
# REPL (interactive)
mxcli -p /path/to/app.mpr
# CLI one-liner
mxcli -p /path/to/app.mpr -c "SHOW ENTITIES"
If you do not have a Mendix project handy, the commands will still make sense – the output format and options are the same regardless of the project content.
SHOW MODULES, SHOW ENTITIES
The SHOW family of commands lists project elements by type. They are the fastest way to see what a project contains.
Listing modules
Every Mendix project is organized into modules. Start by listing them:
SHOW MODULES;
Example output:
MyFirstModule
Administration
Atlas_Core
System
By default, system and marketplace modules (like System and Atlas_Core) are included. The modules appear in the order they are defined in the project.
Listing entities
To see all entities across all modules:
SHOW ENTITIES;
Example output:
Administration.Account
MyFirstModule.Customer
MyFirstModule.Order
MyFirstModule.OrderLine
Each entity is shown as a qualified name – the module name and entity name separated by a dot.
Filtering by module
Most SHOW commands accept an IN clause to filter results to a single module:
SHOW ENTITIES IN MyFirstModule;
MyFirstModule.Customer
MyFirstModule.Order
MyFirstModule.OrderLine
This is typically what you want when working on a specific module.
Listing microflows
SHOW MICROFLOWS;
Administration.ChangeMyPassword
MyFirstModule.ACT_Customer_Save
MyFirstModule.ACT_Order_Process
MyFirstModule.DS_Customer_GetAll
Filter to a module:
SHOW MICROFLOWS IN MyFirstModule;
MyFirstModule.ACT_Customer_Save
MyFirstModule.ACT_Order_Process
MyFirstModule.DS_Customer_GetAll
Listing pages
SHOW PAGES;
Administration.Account_Overview
Administration.Login
MyFirstModule.Customer_Overview
MyFirstModule.Customer_Edit
MyFirstModule.Order_Detail
Filter to a module:
SHOW PAGES IN MyFirstModule;
MyFirstModule.Customer_Overview
MyFirstModule.Customer_Edit
MyFirstModule.Order_Detail
Other SHOW commands
The same pattern works for all major element types:
SHOW ENUMERATIONS;
SHOW ENUMERATIONS IN MyFirstModule;
SHOW ASSOCIATIONS;
SHOW ASSOCIATIONS IN MyFirstModule;
SHOW WORKFLOWS;
SHOW WORKFLOWS IN MyFirstModule;
SHOW NANOFLOWS;
SHOW NANOFLOWS IN MyFirstModule;
SHOW CONSTANTS;
SHOW CONSTANTS IN MyFirstModule;
SHOW SNIPPETS;
SHOW SNIPPETS IN MyFirstModule;
You can also list security-related elements:
SHOW MODULE ROLES;
SHOW MODULE ROLES IN MyFirstModule;
SHOW USER ROLES;
SHOW DEMO USERS;
And navigation:
SHOW NAVIGATION;
Using SHOW from the command line
Every SHOW command works as a CLI one-liner with -c:
mxcli -p app.mpr -c "SHOW ENTITIES"
mxcli -p app.mpr -c "SHOW MICROFLOWS IN MyFirstModule"
mxcli -p app.mpr -c "SHOW PAGES"
This is useful for quick lookups without entering the REPL, and for piping output to other tools:
# Count entities per module
mxcli -p app.mpr -c "SHOW ENTITIES" | cut -d. -f1 | sort | uniq -c
# Find all microflows with "Save" in the name
mxcli -p app.mpr -c "SHOW MICROFLOWS" | grep -i save
Summary of SHOW commands
| Command | Description |
|---|---|
SHOW MODULES | List all modules |
SHOW ENTITIES [IN Module] | List entities |
SHOW MICROFLOWS [IN Module] | List microflows |
SHOW NANOFLOWS [IN Module] | List nanoflows |
SHOW PAGES [IN Module] | List pages |
SHOW SNIPPETS [IN Module] | List snippets |
SHOW ENUMERATIONS [IN Module] | List enumerations |
SHOW ASSOCIATIONS [IN Module] | List associations |
SHOW CONSTANTS [IN Module] | List constants |
SHOW WORKFLOWS [IN Module] | List workflows |
SHOW BUSINESS EVENTS [IN Module] | List business event services |
SHOW JAVA ACTIONS [IN Module] | List Java actions |
SHOW MODULE ROLES [IN Module] | List module roles |
SHOW USER ROLES | List user roles |
SHOW DEMO USERS | List demo users |
SHOW NAVIGATION | Show navigation profiles |
Now that you can list elements, the next step is inspecting individual elements in detail with DESCRIBE and SEARCH.
DESCRIBE, SEARCH
While SHOW commands list elements by name, DESCRIBE gives you the full definition of a single element. SEARCH lets you find elements by keyword when you do not know the exact name.
DESCRIBE ENTITY
To see the complete definition of an entity, including its attributes, types, and constraints:
DESCRIBE ENTITY MyFirstModule.Customer;
Example output:
CREATE PERSISTENT ENTITY MyFirstModule.Customer (
Name: String(200),
Email: String(200),
Phone: String(50),
DateOfBirth: DateTime,
IsActive: Boolean DEFAULT false
);
The output is valid MDL – you could copy it, modify it, and execute it as a CREATE OR MODIFY statement. This round-trip capability is one of the key design features of MDL.
DESCRIBE also shows associations and access rules when they exist:
DESCRIBE ENTITY MyFirstModule.Order;
CREATE PERSISTENT ENTITY MyFirstModule.Order (
OrderNumber: AutoNumber,
OrderDate: DateTime,
TotalAmount: Decimal,
Status: MyFirstModule.OrderStatus
);
CREATE ASSOCIATION MyFirstModule.Order_Customer
FROM MyFirstModule.Order TO MyFirstModule.Customer
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DeleteRefSetOnly;
DESCRIBE MICROFLOW
To see the full logic of a microflow:
DESCRIBE MICROFLOW MyFirstModule.ACT_Customer_Save;
Example output:
CREATE MICROFLOW MyFirstModule.ACT_Customer_Save
($Customer: MyFirstModule.Customer)
RETURNS Boolean
BEGIN
IF $Customer/Name = empty THEN
VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name is required';
RETURN false;
END IF;
COMMIT $Customer;
RETURN true;
END;
This shows the parameters, return type, and the complete flow logic. For complex microflows, this can be quite long – but it gives you the full picture in one command.
DESCRIBE PAGE
Pages are shown as their widget tree:
DESCRIBE PAGE MyFirstModule.Customer_Edit;
Example output:
CREATE PAGE MyFirstModule.Customer_Edit
(
Params: { $Customer: MyFirstModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
};
DESCRIBE ENUMERATION
DESCRIBE ENUMERATION MyFirstModule.OrderStatus;
CREATE ENUMERATION MyFirstModule.OrderStatus (
Draft 'Draft',
Submitted 'Submitted',
Approved 'Approved',
Rejected 'Rejected',
Completed 'Completed'
);
Each value is followed by its caption (the display label shown to end users).
DESCRIBE ASSOCIATION
DESCRIBE ASSOCIATION MyFirstModule.Order_Customer;
CREATE ASSOCIATION MyFirstModule.Order_Customer
FROM MyFirstModule.Order TO MyFirstModule.Customer
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DeleteRefSetOnly;
Other DESCRIBE targets
The same pattern works for other element types:
DESCRIBE WORKFLOW MyFirstModule.ApprovalFlow;
DESCRIBE NANOFLOW MyFirstModule.NAV_GoToDetail;
DESCRIBE SNIPPET MyFirstModule.CustomerCard;
DESCRIBE CONSTANT MyFirstModule.ApiBaseUrl;
DESCRIBE NAVIGATION;
DESCRIBE SETTINGS;
Full-text search with SEARCH
When you do not know the exact name of an element, use SEARCH to find it by keyword. This searches across all strings in the project – entity names, attribute names, microflow logic, page captions, messages, documentation, and more.
SEARCH 'validation';
Example output:
MyFirstModule.ACT_Customer_Save (Microflow)
"VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name is required'"
MyFirstModule.ACT_Order_Validate (Microflow)
"VALIDATION FEEDBACK $Order/OrderDate MESSAGE 'Order date cannot be in the past'"
MyFirstModule.Customer_Edit (Page)
"Validation errors will appear here"
Search is case-insensitive and matches partial words.
Search from the command line
The CLI provides a dedicated search subcommand with formatting options:
# Search with default output
mxcli search -p app.mpr "validation"
# Show only element names (no context)
mxcli search -p app.mpr "validation" --format names
# JSON output for programmatic use
mxcli search -p app.mpr "validation" --format json
The --format names option is useful for piping into other commands:
# Find all microflows mentioning "email" and describe each one
mxcli search -p app.mpr "email" --format names | while read name; do
mxcli -p app.mpr -c "DESCRIBE MICROFLOW $name"
done
Combining SHOW, DESCRIBE, and SEARCH
A typical exploration workflow looks like this:
-
Start broad with SHOW to see what exists:
SHOW MODULES; SHOW ENTITIES IN Sales; -
Zoom in with DESCRIBE on interesting elements:
DESCRIBE ENTITY Sales.Order; DESCRIBE MICROFLOW Sales.ACT_Order_Process; -
Search when you need to find something specific:
SEARCH 'discount';
This workflow mirrors how you would explore a project in Mendix Studio Pro – browsing the project explorer, opening documents, and using Find to locate things.
Next, learn how to get a compact overview of the entire project with SHOW STRUCTURE.
SHOW STRUCTURE
The SHOW STRUCTURE command gives you a compact, tree-style overview of a project. It is the fastest way to understand the overall shape of an application – what modules exist, what types of documents they contain, and how large each module is.
Default view
With no arguments, SHOW STRUCTURE displays all user modules at depth 2 – modules with their documents listed by type:
SHOW STRUCTURE;
Example output:
MyFirstModule
Entities
Customer (Name, Email, Phone, DateOfBirth, IsActive)
Order (OrderNumber, OrderDate, TotalAmount, Status)
OrderLine (Quantity, UnitPrice, LineTotal)
Microflows
ACT_Customer_Save ($Customer: Customer) : Boolean
ACT_Order_Process ($Order: Order) : Boolean
DS_Customer_GetAll () : List of Customer
Pages
Customer_Overview
Customer_Edit ($Customer: Customer)
Order_Detail ($Order: Order)
Enumerations
OrderStatus (Draft, Submitted, Approved, Rejected, Completed)
Administration
Entities
Account (FullName, Email, IsLocalUser, BlockedSince)
Microflows
ChangeMyPassword ($OldPassword, $NewPassword) : Boolean
Pages
Account_Overview
Login
This gives you a bird’s-eye view without opening individual elements. Entity signatures show attribute names; microflow signatures show parameters and return types.
Depth levels
The DEPTH option controls how much detail is shown:
DEPTH 1 – Module summary
Shows one line per module with element counts:
SHOW STRUCTURE DEPTH 1;
MyFirstModule 3 entities, 3 microflows, 3 pages, 1 enumeration, 2 associations
Administration 1 entity, 1 microflow, 2 pages
This is useful for getting a quick sense of project size and where the complexity lives.
DEPTH 2 – Elements with signatures (default)
This is the default when you run SHOW STRUCTURE with no depth specified. It shows modules, their documents grouped by type, and compact signatures for each element. See the example under Default view above.
DEPTH 3 – Full detail
Shows typed attributes and named parameters:
SHOW STRUCTURE DEPTH 3;
MyFirstModule
Entities
Customer
Name: String(200)
Email: String(200)
Phone: String(50)
DateOfBirth: DateTime
IsActive: Boolean DEFAULT false
Order
OrderNumber: AutoNumber
OrderDate: DateTime
TotalAmount: Decimal
Status: MyFirstModule.OrderStatus
OrderLine
Quantity: Integer
UnitPrice: Decimal
LineTotal: Decimal
Microflows
ACT_Customer_Save ($Customer: MyFirstModule.Customer) : Boolean
ACT_Order_Process ($Order: MyFirstModule.Order) : Boolean
DS_Customer_GetAll () : List of MyFirstModule.Customer
Pages
Customer_Overview
Customer_Edit ($Customer: MyFirstModule.Customer)
Order_Detail ($Order: MyFirstModule.Order)
Enumerations
OrderStatus
Draft 'Draft'
Submitted 'Submitted'
Approved 'Approved'
Rejected 'Rejected'
Completed 'Completed'
Associations
Order_Customer: Order -> Customer (Reference)
OrderLine_Order: OrderLine -> Order (Reference)
Depth 3 is verbose but gives you the most complete picture without running individual DESCRIBE commands.
Filtering by module
Use IN to show only a single module:
SHOW STRUCTURE IN MyFirstModule;
This produces the same tree format but limited to one module. Combine with DEPTH for control over detail:
SHOW STRUCTURE DEPTH 3 IN MyFirstModule;
Including system modules
By default, system and marketplace modules are hidden. Add ALL to include them:
SHOW STRUCTURE DEPTH 1 ALL;
MyFirstModule 3 entities, 3 microflows, 3 pages, 1 enumeration, 2 associations
Administration 1 entity, 1 microflow, 2 pages
Atlas_Core 0 entities, 0 microflows, 12 pages
System 15 entities, 0 microflows, 0 pages
This is useful when you need to see system entities (like System.Image or System.FileDocument) or check what a marketplace module provides.
Using SHOW STRUCTURE from the command line
# Quick project overview
mxcli -p app.mpr -c "SHOW STRUCTURE DEPTH 1"
# Detailed view of one module
mxcli -p app.mpr -c "SHOW STRUCTURE DEPTH 3 IN Sales"
# Full project including system modules
mxcli -p app.mpr -c "SHOW STRUCTURE ALL"
When to use SHOW STRUCTURE vs SHOW + DESCRIBE
| Goal | Command |
|---|---|
| “What modules are in this project?” | SHOW STRUCTURE DEPTH 1 |
| “What does module X contain?” | SHOW STRUCTURE IN X |
| “List all entities (just names)” | SHOW ENTITIES |
| “What are the attributes of entity X?” | DESCRIBE ENTITY X |
| “Give me a complete overview of everything” | SHOW STRUCTURE DEPTH 3 |
SHOW STRUCTURE is best for orientation – understanding the shape of the project at a glance. For detailed work on specific elements, switch to DESCRIBE.
What is next
Now that you can explore a project, you are ready to start making changes. Continue to Your First Changes to learn how to create entities, microflows, and pages using MDL.
Your First Changes
Now that you can explore a project with SHOW and DESCRIBE, it’s time to make changes. This chapter walks through the three most common modifications: creating an entity, a microflow, and a page.
Before you start
Always work on a copy. mxcli writes directly to your .mpr file. Before making changes, either:
- Copy the
.mprfile (andmprcontents/folder if it exists) to a scratch directory - Use Git so you can revert with
git checkout
# Make a working copy
cp app.mpr app-scratch.mpr
cp -r mprcontents/ mprcontents-scratch/ # only for MPR v2 projects
The workflow
Every modification follows the same pattern:
- Write MDL – either directly on the command line with
-c, or in a.mdlscript file - Check syntax – run
mxcli checkto catch errors before touching the project - Execute – apply the changes to the
.mprfile - Validate – use
mxcli docker checkfor a full Studio Pro-level validation - Open in Studio Pro – confirm the result visually
# Step 1-2: Write and check syntax
mxcli check setup.mdl
# Step 3: Execute against the project
mxcli exec setup.mdl -p app.mpr
# Step 4: Full validation
mxcli docker check -p app.mpr
# Step 5: Open in Studio Pro and inspect
You can also skip the script file and execute commands directly:
mxcli -p app.mpr -c "CREATE PERSISTENT ENTITY MyModule.Product (Name: String(200) NOT NULL, Price: Decimal);"
What we’ll build
Over the next few pages, you’ll create:
| What | MDL statement | Page |
|---|---|---|
A Product entity with attributes | CREATE PERSISTENT ENTITY | Creating an Entity |
| A microflow that creates products | CREATE MICROFLOW | Creating a Microflow |
| An overview page listing products | CREATE PAGE | Creating a Page |
The final page, Validating with mxcli check, covers the full validation workflow in detail.
Quick note on module names
Every MDL statement references a module. In these examples we use MyModule – replace it with whatever module exists in your project. You can check available modules with:
mxcli -p app.mpr -c "SHOW MODULES"
Idempotent scripts with OR MODIFY
If you plan to run a script more than once (common during development), use CREATE OR MODIFY instead of plain CREATE. This updates the entity if it already exists instead of failing:
CREATE OR MODIFY PERSISTENT ENTITY MyModule.Product (
Name: String(200) NOT NULL,
Price: Decimal,
IsActive: Boolean DEFAULT true
);
The OR MODIFY variant is available for entities, enumerations, microflows, and pages. You’ll see it used throughout the tutorial.
Creating an Entity
Entities are the foundation of any Mendix application – they define your data model. In this page you’ll create a Product entity with several attributes, verify it, and then link it to another entity with an association.
Create a simple entity
The CREATE PERSISTENT ENTITY statement defines an entity that is stored in the database. Attributes go inside parentheses, separated by commas:
CREATE PERSISTENT ENTITY MyModule.Product (
Name: String(200) NOT NULL,
Price: Decimal,
IsActive: Boolean DEFAULT true
);
Let’s break this down:
| Part | Meaning |
|---|---|
PERSISTENT | The entity is stored in the database (as opposed to NON-PERSISTENT, which exists only in memory) |
MyModule.Product | Fully qualified name: module dot entity name |
String(200) | A string attribute with a maximum length of 200 characters |
NOT NULL | The attribute is required – it cannot be left empty |
Decimal | A decimal number (no precision/scale needed for basic use) |
Boolean DEFAULT true | A boolean that defaults to true when a new object is created |
Run it from the command line:
mxcli -p app.mpr -c "CREATE PERSISTENT ENTITY MyModule.Product (Name: String(200) NOT NULL, Price: Decimal, IsActive: Boolean DEFAULT true);"
Or save it to a file and execute:
mxcli exec create-product.mdl -p app.mpr
Verify the entity
Use DESCRIBE ENTITY to confirm your entity was created correctly:
mxcli -p app.mpr -c "DESCRIBE ENTITY MyModule.Product"
This prints the full MDL definition of the entity, including all attributes and their types. You should see Name, Price, and IsActive listed.
Add an association
Associations link entities together. To connect Product to an existing Order entity, create a reference association:
CREATE ASSOCIATION MyModule.Order_Product
FROM MyModule.Order TO MyModule.Product
TYPE Reference;
This creates a many-to-one relationship: each Order can reference one Product. Use ReferenceSet instead of Reference for many-to-many.
The naming convention Order_Product follows Mendix best practices – the “from” entity name comes first, then the “to” entity.
Association types
| Type | Meaning | Example |
|---|---|---|
Reference | Many-to-one (or one-to-one) | An Order references one Product |
ReferenceSet | Many-to-many | An Order can have multiple Products |
Delete behavior
You can specify what happens when the “to” entity is deleted:
CREATE ASSOCIATION MyModule.Order_Product
FROM MyModule.Order TO MyModule.Product
TYPE Reference
DELETE_BEHAVIOR PREVENT;
Options: PREVENT (block deletion if referenced), DELETE (cascade delete), or leave it out for the default behavior.
Using OR MODIFY for idempotent scripts
If you want to run the same script repeatedly without errors, use CREATE OR MODIFY:
CREATE OR MODIFY PERSISTENT ENTITY MyModule.Product (
Name: String(200) NOT NULL,
Price: Decimal,
IsActive: Boolean DEFAULT true,
Description: String(unlimited)
);
If the entity already exists, this updates it to match the new definition. If it doesn’t exist, it creates it. This is especially useful during iterative development.
More attribute types
Here’s a fuller example showing the attribute types you’ll use most often:
CREATE PERSISTENT ENTITY MyModule.Product (
Name: String(200) NOT NULL,
Description: String(unlimited),
Price: Decimal,
Quantity: Integer,
Weight: Long,
IsActive: Boolean DEFAULT true,
CreatedDate: DateTime,
Status: MyModule.ProductStatus
);
The last attribute references an enumeration (MyModule.ProductStatus). You’d create that separately:
CREATE ENUMERATION MyModule.ProductStatus (
Active 'Active',
Discontinued 'Discontinued',
OutOfStock 'Out of Stock'
);
Extending a system entity
To create an entity that inherits from a system entity (like System.Image for file storage), use EXTENDS. Note that EXTENDS must come before the opening parenthesis:
-- Correct: EXTENDS before (
CREATE PERSISTENT ENTITY MyModule.ProductPhoto EXTENDS System.Image (
PhotoCaption: String(200)
);
-- Wrong: EXTENDS after ( -- this will cause a parse error
CREATE PERSISTENT ENTITY MyModule.ProductPhoto (
PhotoCaption: String(200)
) EXTENDS System.Image;
Common mistakes
String needs an explicit length. String alone is not valid – you must specify the maximum length:
-- Wrong
Name: String
-- Correct
Name: String(200)
-- For unlimited length
Description: String(unlimited)
EXTENDS must come before the parenthesis. See the example above. This is a common source of parse errors.
Module must exist. The module in the qualified name (e.g., MyModule in MyModule.Product) must already exist in the project. Check with SHOW MODULES.
Creating a Microflow
Microflows are the server-side logic of a Mendix application. They’re comparable to functions or methods in traditional programming. In this page you’ll create a microflow that accepts parameters, creates an object, commits it to the database, and returns it.
Create a simple microflow
This microflow takes a name and price, creates a Product object, commits it, and returns it:
CREATE MICROFLOW MyModule.CreateProduct(
DECLARE $Name: String,
DECLARE $Price: Decimal
)
RETURN MyModule.Product
BEGIN
CREATE $Product: MyModule.Product (
Name = $Name,
Price = $Price,
IsActive = true
);
COMMIT $Product;
RETURN $Product;
END;
Let’s walk through each part:
| Part | Meaning |
|---|---|
MyModule.CreateProduct | Fully qualified microflow name |
DECLARE $Name: String | Input parameter – a string value passed by the caller |
RETURN MyModule.Product | The microflow returns a Product entity object |
BEGIN ... END | The microflow body |
CREATE $Product: MyModule.Product (...) | Creates a new Product object in memory and sets its attributes |
COMMIT $Product | Persists the object to the database |
RETURN $Product | Returns the committed object to the caller |
Save this to a file (e.g., create-product-mf.mdl) and execute:
mxcli check create-product-mf.mdl
mxcli exec create-product-mf.mdl -p app.mpr
Verify the microflow
Use DESCRIBE MICROFLOW to see the generated MDL:
mxcli -p app.mpr -c "DESCRIBE MICROFLOW MyModule.CreateProduct"
This shows the full microflow definition, including parameters, activities, and the return type.
Understanding variables
All variables in MDL start with $. There are two contexts where you declare them:
Parameters (in the signature):
CREATE MICROFLOW MyModule.DoSomething(
DECLARE $Customer: MyModule.Customer,
DECLARE $Count: Integer
)
Local variables (inside BEGIN…END):
BEGIN
DECLARE $Total: Integer = 0;
DECLARE $Message: String = 'Processing...';
DECLARE $Items: List of MyModule.Item = empty;
END;
For entity parameters, do not assign a default value – just declare the type:
-- Correct: entity parameter with no default
DECLARE $Customer: MyModule.Customer
-- Wrong: entity parameter with = empty
DECLARE $Customer: MyModule.Customer = empty
Retrieving objects
Use RETRIEVE to fetch objects from the database:
CREATE MICROFLOW MyModule.GetActiveProducts()
RETURN List of MyModule.Product
BEGIN
RETRIEVE $Products: List of MyModule.Product
FROM MyModule.Product
WHERE IsActive = true;
RETURN $Products;
END;
To retrieve a single object, add LIMIT 1:
RETRIEVE $Product: MyModule.Product
FROM MyModule.Product
WHERE Name = $SearchName
LIMIT 1;
Conditional logic
Use IF ... THEN ... ELSE ... END IF for branching:
CREATE MICROFLOW MyModule.UpdateProductStatus(
DECLARE $Product: MyModule.Product
)
RETURN Boolean
BEGIN
IF $Product/Price > 0 THEN
CHANGE $Product (IsActive = true);
COMMIT $Product;
RETURN true;
ELSE
LOG WARNING 'Product has no price set';
RETURN false;
END IF;
END;
Looping over a list
Use LOOP ... IN ... BEGIN ... END LOOP to iterate:
CREATE MICROFLOW MyModule.DeactivateProducts(
DECLARE $Products: List of MyModule.Product
)
RETURN Boolean
BEGIN
LOOP $Product IN $Products
BEGIN
CHANGE $Product (IsActive = false);
COMMIT $Product;
END LOOP;
RETURN true;
END;
Note: the list $Products is a parameter. Never create an empty list variable and then loop over it – accept the list as a parameter instead.
Error handling
Add ON ERROR to handle failures on individual activities:
COMMIT $Product ON ERROR CONTINUE;
Options are CONTINUE (ignore the error and proceed), ROLLBACK (roll back the transaction and continue), or an inline error handler block:
COMMIT $Product ON ERROR {
LOG ERROR 'Failed to commit product';
RETURN false;
};
Organizing with folders
Place microflows in folders using the FOLDER keyword:
CREATE MICROFLOW MyModule.CreateProduct(
DECLARE $Name: String,
DECLARE $Price: Decimal
)
RETURN MyModule.Product
FOLDER 'ACT'
BEGIN
CREATE $Product: MyModule.Product (
Name = $Name,
Price = $Price,
IsActive = true
);
COMMIT $Product;
RETURN $Product;
END;
This places the microflow in the ACT folder within the module, following the common Mendix convention of grouping microflows by type (ACT for actions, VAL for validations, SUB for sub-microflows).
Common mistakes
Every flow path must end with RETURN. If your microflow has IF/ELSE branches, each branch needs its own RETURN statement (or the return can come after END IF if both branches converge).
COMMIT is required to persist changes. CREATE and CHANGE only modify the object in memory. Without COMMIT, changes are lost when the microflow ends.
Entity parameters don’t use = empty. Declare them with just the type. Assigning = empty is for list variables inside the microflow body, not for parameters.
Creating a Page
Pages are the user interface of a Mendix application. In this page you’ll create an overview page that lists products in a data grid, and then an edit page with a form for modifying a single product.
Prerequisites
Before creating a page, you need two things:
- An entity – the page needs data to display. We’ll use the
MyModule.Productentity from the previous steps. - A layout – every page is placed inside a layout that provides the overall page structure (header, sidebar, content area). The layout must already exist in your project.
To see what layouts are available:
mxcli -p app.mpr -c "SHOW PAGES IN Atlas_Core"
Most Mendix projects based on Atlas UI have layouts like Atlas_Core.Atlas_Default (full page), Atlas_Core.PopupLayout (dialog), and others.
Create an overview page
An overview page typically shows a data grid that lists all objects of an entity:
CREATE PAGE MyModule.ProductOverview
LAYOUT Atlas_Core.Atlas_Default
TITLE 'Products'
(
DATAGRID SOURCE DATABASE MyModule.Product (
COLUMN Name,
COLUMN Price,
COLUMN IsActive
)
);
Let’s break this down:
| Part | Meaning |
|---|---|
MyModule.ProductOverview | Fully qualified page name |
LAYOUT Atlas_Core.Atlas_Default | The layout this page uses – must exist in the project |
TITLE 'Products' | The page title shown in the browser tab and header |
DATAGRID SOURCE DATABASE MyModule.Product | A data grid that loads Product objects from the database |
COLUMN Name | A column showing the Name attribute |
Execute it:
mxcli -p app.mpr -c "CREATE PAGE MyModule.ProductOverview LAYOUT Atlas_Core.Atlas_Default TITLE 'Products' (DATAGRID SOURCE DATABASE MyModule.Product (COLUMN Name, COLUMN Price, COLUMN IsActive));"
Or save to a file and run:
mxcli check create-pages.mdl
mxcli exec create-pages.mdl -p app.mpr
Verify the page
mxcli -p app.mpr -c "DESCRIBE PAGE MyModule.ProductOverview"
This prints the full page definition with all widgets and their properties.
Create an edit page
Edit pages use a DataView to display and edit a single object. The object is passed as a page parameter:
CREATE PAGE MyModule.Product_Edit
(
Params: { $Product: MyModule.Product },
Title: 'Edit Product',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvProduct (DataSource: $Product) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtPrice (Label: 'Price', Attribute: Price)
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
};
Key differences from the overview page:
| Part | Meaning |
|---|---|
Params: { $Product: MyModule.Product } | The page expects a Product object to be passed when opened |
Layout: Atlas_Core.PopupLayout | Uses a popup/dialog layout instead of a full page |
DATAVIEW dvProduct (DataSource: $Product) | Binds to the page parameter |
TEXTBOX, CHECKBOX | Input widgets bound to entity attributes |
FOOTER | A section at the bottom of the DataView for action buttons |
Action: SAVE_CHANGES | Built-in action that commits the object and closes the page |
Action: CANCEL_CHANGES | Built-in action that rolls back changes and closes the page |
Notice the two different page syntaxes: the overview page uses the compact syntax (LAYOUT and TITLE as keywords before parentheses), while the edit page uses the property syntax (properties inside a (Key: value) block followed by a { widget tree } block). Both are valid – use whichever fits better.
Widget reference
Here are the most commonly used widgets:
Input widgets
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTAREA txtDesc (Label: 'Description', Attribute: Description)
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
DATEPICKER dpCreated (Label: 'Created', Attribute: CreatedDate)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
RADIOBUTTONS rbType (Label: 'Type', Attribute: ProductType)
Display widgets
DYNAMICTEXT dynName (Attribute: Name)
Action buttons
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
ACTIONBUTTON btnProcess (Caption: 'Process', Action: MICROFLOW MyModule.ACT_ProcessProduct(Product: $Product))
Layout widgets
CONTAINER cntWrapper (Class: 'card') {
-- child widgets go here
}
LAYOUTGRID lgMain {
ROW r1 {
COLUMN c1 (Weight: 6) { ... }
COLUMN c2 (Weight: 6) { ... }
}
}
Embedding a snippet
To reuse a page fragment, call a snippet:
SNIPPETCALL scProductCard (Snippet: MyModule.ProductCard)
The snippet must already exist in the project.
Common mistakes
Layout must exist in the project. If you specify a layout that doesn’t exist, the page will fail validation. Check available layouts with SHOW PAGES and look for documents of type Layout.
Widget names must be unique within a page. Every widget needs a name (e.g., txtName, btnSave), and these must not collide within the same page.
DataView needs a data source. A DataView must know where its data comes from – either a page parameter (DataSource: $Product), a microflow, or a nested context.
Validating with mxcli check
You’ve created entities, microflows, and pages. Before opening your project in Studio Pro, you should validate that everything is correct. mxcli provides three levels of validation, each catching different kinds of problems.
Level 1: Syntax check (no project needed)
The fastest check. It parses your MDL script and reports syntax errors without touching any .mpr file:
mxcli check script.mdl
This catches:
- Typos in keywords (
CRETEinstead ofCREATE) - Missing semicolons or parentheses
- Invalid attribute type syntax (
Stringwithout a length) - Anti-patterns like empty list variables used as loop sources
- Nested loops that should use
RETRIEVEfor lookups
You don’t even need a project file for this. It’s a pure syntax and structure check, so it runs instantly.
Checking inline commands
You can also check a single statement:
mxcli check -c "CREATE PERSISTENT ENTITY MyModule.Product (Name: String(200));"
Level 2: Syntax + reference validation
Add -p and --references to also verify that names in your script resolve to real elements in the project:
mxcli check script.mdl -p app.mpr --references
This catches everything Level 1 catches, plus:
- References to entities that don’t exist (
MyModule.NonExistent) - References to attributes that don’t exist on an entity
- Microflow calls to non-existent microflows
- Page layouts that don’t exist in the project
- Association endpoints pointing to missing entities
This is the check you should run before executing a script. It’s fast (reads the project but doesn’t modify it) and catches most mistakes.
Level 3: Full project validation with mx check
After executing your script, validate the entire project using the Mendix toolchain:
mxcli docker check -p app.mpr
This runs Mendix’s own mx check tool inside a Docker container. It performs the same validation that Studio Pro does when you open a project, catching issues that mxcli’s parser cannot detect:
- BSON serialization errors (malformed internal data)
- Security configuration problems (missing access rules, CE0066 errors)
- Widget definition mismatches (CE0463 errors)
- Missing required properties on widgets
- Broken cross-references between documents
The first time you run this, it will download the MxBuild toolchain for your project’s Mendix version. Subsequent runs reuse the cached download.
Without Docker
If you have mx installed locally (e.g., from a Mendix installation), you can run the check directly:
~/.mxcli/mxbuild/*/modeler/mx check app.mpr
Or use mxcli to auto-download and run it:
mxcli setup mxbuild -p app.mpr
~/.mxcli/mxbuild/*/modeler/mx check app.mpr
The recommended workflow
Here’s the workflow you should follow for every change:
Write MDL --> Check syntax --> Execute --> Docker check --> Open in Studio Pro
In practice:
# 1. Write your MDL in a script file
cat > changes.mdl << 'EOF'
CREATE OR MODIFY PERSISTENT ENTITY MyModule.Product (
Name: String(200) NOT NULL,
Price: Decimal,
IsActive: Boolean DEFAULT true
);
CREATE OR MODIFY MICROFLOW MyModule.CreateProduct(
DECLARE $Name: String,
DECLARE $Price: Decimal
)
RETURN MyModule.Product
BEGIN
CREATE $Product: MyModule.Product (
Name = $Name,
Price = $Price,
IsActive = true
);
COMMIT $Product;
RETURN $Product;
END;
EOF
# 2. Check syntax (fast, no project needed)
mxcli check changes.mdl
# 3. Check references (needs project, still fast)
mxcli check changes.mdl -p app.mpr --references
# 4. Execute against the project
mxcli exec changes.mdl -p app.mpr
# 5. Full validation
mxcli docker check -p app.mpr
# 6. Open in Studio Pro and verify visually
Previewing changes with diff
Before executing, you can preview what a script would change:
mxcli diff -p app.mpr changes.mdl
This compares the script against the current project state and shows what would be created, modified, or left unchanged. It does not modify the project.
What mxcli check catches automatically
Beyond basic syntax, mxcli check includes built-in anti-pattern detection:
| Pattern | Problem | What check reports |
|---|---|---|
DECLARE $Items List of ... = empty followed by LOOP $Item IN $Items | Looping over an empty list does nothing | Warning: empty list variable used as loop source |
Nested LOOP inside LOOP for list matching | O(N^2) performance | Warning: use RETRIEVE from list instead |
Missing RETURN at end of flow path | Microflow won’t compile | Error: missing return statement |
Linting for deeper analysis
For a broader set of checks across the entire project (not just a single script), use the linter:
mxcli lint -p app.mpr
This runs 14 built-in rules plus 27 Starlark rules covering security, architecture, quality, and naming conventions. See mxcli lint --list-rules for the full list.
For CI/CD integration, output in SARIF format:
mxcli lint -p app.mpr --format sarif > results.sarif
Working with AI Assistants
mxcli is built from the ground up to work with AI coding assistants. The mxcli init command sets up your Mendix project with skills, configuration files, and a dev container so that AI tools can read and modify your project using MDL.
Why AI + MDL?
Mendix projects are stored in binary .mpr files. AI assistants cannot read or edit binary files directly. mxcli solves this by providing MDL – a text-based, SQL-like language that describes Mendix model elements. An AI assistant uses mxcli commands to explore the project, writes MDL scripts to make changes, validates them, and executes them against the .mpr file.
The result: you describe what you want in natural language, and the AI builds it in your Mendix project.
The token efficiency advantage
When working with AI APIs, context window size is a critical constraint. MDL’s compact syntax provides a significant advantage over JSON model representations:
| Representation | Tokens for a 10-entity module |
|---|---|
| JSON (raw model) | ~15,000–25,000 tokens |
| MDL | ~2,000–4,000 tokens |
| Savings | 5–10x fewer tokens |
Fewer tokens means lower API costs, more of your application fits in a single prompt, and the AI produces better results because there is less noise in the context.
Supported AI tools
mxcli supports five AI coding assistants out of the box, plus a universal format that works with any tool:
| Tool | Init Flag | Config File | Description |
|---|---|---|---|
| Claude Code | --tool claude (default) | .claude/, CLAUDE.md | Full integration with skills, commands, and lint rules |
| Cursor | --tool cursor | .cursorrules | Compact MDL reference and command guide |
| Continue.dev | --tool continue | .continue/config.json | Custom commands and slash commands |
| Windsurf | --tool windsurf | .windsurfrules | Codeium’s AI with MDL rules |
| Aider | --tool aider | .aider.conf.yml | Terminal-based AI pair programming |
| Universal | (always created) | AGENTS.md, .ai-context/ | Works with all tools |
# List all supported tools
mxcli init --list-tools
How it works at a glance
The workflow is the same regardless of which AI tool you use:
- Initialize –
mxcli initcreates config files, skills, and a dev container - Open in dev container – sandboxes the AI so it only accesses your project
- Start the AI assistant – Claude Code, Cursor, Continue.dev, etc.
- Ask for changes in natural language – “Create a Customer entity with name and email”
- The AI explores – it runs SHOW, DESCRIBE, and SEARCH commands via mxcli
- The AI writes MDL – guided by the skill files installed in your project
- The AI validates –
mxcli checkcatches syntax errors before anything is applied - The AI executes – the MDL script is run against your
.mprfile - You review in Studio Pro – open the project and verify the result
The next pages cover each tool in detail, starting with Claude Code.
Claude Code Integration
Claude Code is the primary AI integration for mxcli. It gets the deepest support: a dedicated configuration directory, project-level context in CLAUDE.md, skill files that teach Claude MDL patterns, and slash commands for common operations.
Initializing a project
Claude Code is the default tool, so you don’t need to specify a flag:
mxcli init /path/to/my-mendix-project
This is equivalent to:
mxcli init --tool claude /path/to/my-mendix-project
What gets created
After running mxcli init, your project directory gains several new entries:
my-mendix-project/
├── CLAUDE.md # Project context for Claude Code
├── AGENTS.md # Universal AI assistant guide
├── .claude/
│ ├── settings.json # Claude Code settings
│ ├── commands/ # Slash commands (/create-entity, etc.)
│ └── lint-rules/ # Starlark lint rules
├── .ai-context/
│ ├── skills/ # MDL pattern guides (shared by all tools)
│ └── examples/ # Example MDL scripts
├── .devcontainer/
│ ├── devcontainer.json # Dev container configuration
│ └── Dockerfile # Container image with mxcli, JDK, Docker
├── mxcli # CLI binary (copied into project)
└── app.mpr # Your Mendix project (already existed)
CLAUDE.md
The CLAUDE.md file gives Claude Code project-level context. It describes what mxcli is, lists the available MDL commands, and tells Claude to read the skill files before writing MDL. This file is automatically read by Claude Code when it starts.
Skills
The .claude/skills/ directory (and .ai-context/skills/ for the universal copy) contains markdown files that teach Claude specific MDL patterns. For example, write-microflows.md explains microflow syntax, common mistakes, and a validation checklist. Claude reads the relevant skill before generating MDL, which dramatically improves output quality.
Commands
The .claude/commands/ directory contains slash commands that you can invoke from within Claude Code. These provide shortcuts for common operations.
Setting up the dev container
The dev container is the recommended way to work with Claude Code. It sandboxes the AI so it can only access your project files, and it comes pre-configured with everything you need.
What’s installed in the container
| Component | Purpose |
|---|---|
| mxcli | Mendix CLI (copied into project root) |
| MxBuild / mx | Mendix project validation and building |
| JDK 21 (Adoptium) | Required by MxBuild |
| Docker-in-Docker | Running Mendix apps locally with mxcli docker |
| Node.js | Playwright testing support |
| PostgreSQL client | Database connectivity |
| Claude Code | Auto-installed when the container starts |
Opening the dev container
- Open your project folder in VS Code
- VS Code detects the
.devcontainer/directory and shows a notification - Click “Reopen in Container” (or use the command palette:
Dev Containers: Reopen in Container) - Wait for the container to build (first time takes a few minutes)
- Once inside the container, open a terminal
Starting Claude Code
With the dev container running, open a terminal in VS Code and start Claude:
claude
Claude Code now has access to your project files, the mxcli binary, and all the skill files. You can start asking it to do things.
How Claude works with your project
Claude follows a consistent pattern when you ask it to modify your Mendix project:
1. Explore
Claude uses mxcli commands to understand your project before making changes:
-- What modules exist?
SHOW MODULES;
-- What entities are in this module?
SHOW ENTITIES IN Sales;
-- What does this entity look like?
DESCRIBE ENTITY Sales.Customer;
-- What microflows exist?
SHOW MICROFLOWS IN Sales;
-- Search for something specific
SEARCH 'validation';
2. Read the relevant skill
Before writing MDL, Claude reads the appropriate skill file. If you ask for a microflow, it reads write-microflows.md. If you ask for a page, it reads create-page.md. The skills contain syntax references, examples, and validation checklists.
3. Write MDL
Claude generates an MDL script based on what it learned from the project and the skill files:
/** Customer master data */
@Position(100, 100)
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200) NOT NULL,
Email: String(200) NOT NULL,
Phone: String(50),
IsActive: Boolean DEFAULT true
);
4. Validate
Claude checks the script for syntax errors and reference issues:
./mxcli check script.mdl
./mxcli check script.mdl -p app.mpr --references
5. Execute
If validation passes, Claude runs the script against your project:
./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'"
6. Verify
Claude can run a full project check using the Mendix build tools:
./mxcli docker check -p app.mpr
Example interaction
Here is a typical conversation with Claude Code:
You: Create a Customer entity in the Sales module with name, email, and phone. Then create an overview page that shows all customers in a data grid.
Claude (explores the project):
./mxcli -p app.mpr -c "SHOW MODULES"
./mxcli -p app.mpr -c "SHOW ENTITIES IN Sales"
./mxcli -p app.mpr -c "SHOW PAGES IN Sales"
Claude (reads skills, writes MDL, validates, and executes):
/** Customer contact information */
@Position(100, 100)
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200) NOT NULL,
Email: String(200) NOT NULL,
Phone: String(50),
IsActive: Boolean DEFAULT true,
CreatedAt: DateTime DEFAULT '[%CurrentDateTime%]'
);
/** Customer overview page with data grid */
CREATE PAGE Sales.Customer_Overview
(
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default
)
{
LAYOUTGRID mainGrid {
ROW row1 {
COLUMN col1 (DesktopWidth: 12) {
DATAGRID dgCustomers (
DataSource: DATABASE FROM Sales.Customer SORT BY Name ASC
) {
COLUMN colName (Attribute: Name, Caption: 'Name')
COLUMN colEmail (Attribute: Email, Caption: 'Email')
COLUMN colPhone (Attribute: Phone, Caption: 'Phone')
COLUMN colActive (Attribute: IsActive, Caption: 'Active')
}
}
}
}
}
Claude validates the script, executes it, and reports back. You can then open the project in Studio Pro to review the result.
Tips for working with Claude Code
- Be specific about module names. Say “Create a Customer entity in the Sales module” rather than just “Create a Customer entity.”
- Mention existing elements. If you want an association to an existing entity, name it: “Link Order to the existing Sales.Customer entity.”
- Let Claude explore first. If you’re asking for changes to an existing project, Claude will run SHOW and DESCRIBE commands to understand what’s already there. This leads to better results than trying to describe the current state yourself.
- Review in Studio Pro. After Claude makes changes, open the project in Studio Pro to verify everything looks right visually.
- Use
mxcli docker checkto catch issues thatmxcli checkalone might miss. The Mendix build tools perform deeper validation.
Next steps
If you use other AI tools alongside Claude Code, see Cursor / Continue.dev / Windsurf. To understand how skills work in detail, see Skills and CLAUDE.md.
Cursor / Continue.dev / Windsurf
Claude Code is the default integration, but mxcli also supports Cursor, Continue.dev, Windsurf, and Aider. Each tool gets its own configuration file that teaches the AI about MDL syntax and mxcli commands.
Initializing for a specific tool
Use the --tool flag to specify which AI tool you use:
# Cursor
mxcli init --tool cursor /path/to/my-mendix-project
# Continue.dev
mxcli init --tool continue /path/to/my-mendix-project
# Windsurf
mxcli init --tool windsurf /path/to/my-mendix-project
# Aider
mxcli init --tool aider /path/to/my-mendix-project
Setting up multiple tools
You can configure several tools at once. This is useful if different team members use different editors, or if you want to try several tools on the same project:
# Multiple tools
mxcli init --tool claude --tool cursor /path/to/my-mendix-project
# All supported tools at once
mxcli init --all-tools /path/to/my-mendix-project
Adding a tool to an existing project
If you already ran mxcli init and want to add support for another tool without re-initializing:
mxcli add-tool cursor
mxcli add-tool windsurf
This creates the tool-specific config file without touching the existing setup.
What each tool gets
Every tool gets the universal files that are always created:
| File | Purpose |
|---|---|
AGENTS.md | Comprehensive AI assistant guide (works with any tool) |
.ai-context/skills/ | MDL pattern guides shared by all tools |
.ai-context/examples/ | Example MDL scripts |
.devcontainer/ | Dev container configuration |
mxcli | CLI binary |
On top of the universal files, each tool gets its own configuration:
| Tool | Config File | Contents |
|---|---|---|
| Claude Code | .claude/, CLAUDE.md | Settings, skills, commands, lint rules, project context |
| Cursor | .cursorrules | Compact MDL reference and mxcli command guide |
| Continue.dev | .continue/config.json | Custom commands and slash commands |
| Windsurf | .windsurfrules | MDL rules for Codeium’s AI |
| Aider | .aider.conf.yml | YAML configuration for Aider |
Tool details
Cursor
Cursor reads its instructions from .cursorrules in the project root. The file mxcli generates contains a compact MDL syntax reference and a list of mxcli commands the AI can use. Cursor’s Composer and Chat features will reference this file automatically.
mxcli init --tool cursor /path/to/project
Created files:
.cursorrules– MDL syntax, command reference, and conventionsAGENTS.md– universal guide (Cursor also reads this).ai-context/skills/– shared skill files
To use: open the project in Cursor, then use Composer (Ctrl+I) or Chat (Ctrl+L) to ask for Mendix changes.
Continue.dev
Continue.dev uses a JSON configuration file with custom commands. The generated config tells Continue about mxcli and provides slash commands for common MDL operations.
mxcli init --tool continue /path/to/project
Created files:
.continue/config.json– custom commands, slash command definitionsAGENTS.mdand.ai-context/skills/– universal files
To use: open the project in VS Code with the Continue extension, then use the Continue sidebar to ask for changes.
Windsurf
Windsurf reads .windsurfrules from the project root. The generated file contains MDL rules and mxcli command documentation tailored for Codeium’s AI.
mxcli init --tool windsurf /path/to/project
Created files:
.windsurfrules– MDL rules and command referenceAGENTS.mdand.ai-context/skills/– universal files
To use: open the project in Windsurf, then use Cascade to ask for Mendix changes.
Aider
Aider is a terminal-based AI pair programming tool. The generated YAML config tells Aider about the project structure and available commands.
mxcli init --tool aider /path/to/project
Created files:
.aider.conf.yml– Aider configurationAGENTS.mdand.ai-context/skills/– universal files
To use: run aider in the project directory from the terminal.
The universal format: AGENTS.md
Regardless of which tool you pick, mxcli always creates AGENTS.md and the .ai-context/ directory. These use a universal format that most AI tools understand:
AGENTS.mdis a comprehensive guide placed in the project root. It describes mxcli, lists MDL commands, and explains the development workflow. Many AI tools (GitHub Copilot, OpenAI Codex, and others) automatically read markdown files in the project root..ai-context/skills/contains the same skill files that Claude Code gets, but in the shared location. Any tool that can read project files can reference these.
This means even if your AI tool is not in the supported list, it can still benefit from mxcli init. The AGENTS.md file and skill directory provide enough context for any AI assistant to work with MDL.
Listing supported tools
To see all tools mxcli knows about:
mxcli init --list-tools
Next steps
To understand what the skill files contain and how they guide AI behavior, see Skills and CLAUDE.md.
Skills and CLAUDE.md
Skills are markdown files that teach AI assistants how to write correct MDL. Each skill covers a specific topic – creating pages, writing microflows, managing security – and contains syntax references, examples, and validation checklists. When an AI assistant needs to generate MDL, it reads the relevant skill first, which dramatically improves output quality.
Where skills live
Skills are installed in two locations:
| Location | Used by |
|---|---|
.claude/skills/ | Claude Code (tool-specific copy) |
.ai-context/skills/ | All tools (universal copy) |
Both directories contain the same files. The .claude/skills/ copy exists because Claude Code has a built-in mechanism for reading files from its .claude/ directory. The .ai-context/skills/ copy is the universal location that any AI tool can access.
Available skills
mxcli init installs the following skill files:
| Skill File | Topic |
|---|---|
generate-domain-model.md | Entity, attribute, and association syntax |
write-microflows.md | Microflow syntax, activities, common mistakes |
create-page.md | Page and widget syntax reference |
alter-page.md | ALTER PAGE/SNIPPET for modifying existing pages |
overview-pages.md | CRUD page patterns (overview + edit) |
master-detail-pages.md | Master-detail page patterns |
manage-security.md | Module roles, user roles, access control, GRANT/REVOKE |
manage-navigation.md | Navigation profiles, home pages, menus |
demo-data.md | Mendix ID system, association storage, demo data insertion |
xpath-constraints.md | XPath syntax in WHERE clauses, nested predicates |
database-connections.md | External database connections from microflows |
check-syntax.md | Pre-flight validation checklist |
organize-project.md | Folders, MOVE command, project structure conventions |
test-microflows.md | Test annotations, file formats, Docker setup |
patterns-data-processing.md | Delta merge, batch processing, list operations |
What a skill file contains
A typical skill file has four sections:
1. Syntax reference
The core MDL syntax for the topic, with all available options and keywords:
-- From write-microflows.md:
CREATE MICROFLOW Module.Name(
$param: EntityType
) RETURNS ReturnType AS $result
BEGIN
-- activities here
END;
2. Examples
Complete, working MDL examples that the AI can use as templates:
-- From create-page.md:
CREATE PAGE Sales.Customer_Overview
(
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default
)
{
DATAGRID dgCustomers (
DataSource: DATABASE FROM Sales.Customer SORT BY Name ASC
) {
COLUMN colName (Attribute: Name, Caption: 'Name')
}
}
3. Common mistakes
A list of errors the AI should avoid. For example, write-microflows.md warns against creating empty list variables as loop sources, and create-page.md documents required widget properties that are easy to forget.
4. Validation checklist
Steps the AI should follow after writing MDL to confirm correctness:
# Syntax check (no project needed)
./mxcli check script.mdl
# Syntax + reference validation
./mxcli check script.mdl -p app.mpr --references
CLAUDE.md
The CLAUDE.md file is specific to Claude Code. It sits in the project root and is automatically read by Claude when it starts. It provides:
- Project overview – what the project is, which modules exist, and what mxcli is
- Available commands – a summary of mxcli commands Claude can use
- Rules – instructions like “always read the relevant skill file before writing MDL” and “always validate with
mxcli checkbefore executing” - Conventions – project-specific naming conventions, module structure, etc.
Think of CLAUDE.md as the “system prompt” for Claude Code in the context of your project. It sets the tone and establishes guardrails.
AGENTS.md
AGENTS.md serves the same purpose as CLAUDE.md but in a universal format. It is always created by mxcli init, regardless of which tool you selected. AI tools that don’t have their own config format (or that read markdown files from the project root) will pick up AGENTS.md automatically.
Adding custom skills
You can create your own skill files to teach the AI about your project’s patterns and conventions. Add markdown files to .ai-context/skills/ (or .claude/skills/ for Claude Code):
.ai-context/skills/
├── write-microflows.md # Built-in (installed by mxcli init)
├── create-page.md # Built-in
├── our-naming-conventions.md # Custom: your team's naming rules
├── order-processing-pattern.md # Custom: how orders work in your app
└── api-integration-guide.md # Custom: how to call external APIs
A custom skill file is just a markdown document. Write it the same way you would explain something to a new team member:
# Order Processing Pattern
When creating microflows that process orders in our application, follow these rules:
1. Always validate the order has at least one OrderLine
2. Use the Sales.OrderStatus enumeration for status tracking
3. Log to the 'OrderProcessing' node at INFO level
4. Send a confirmation email via Sales.SendNotification
## Example
\```sql
CREATE MICROFLOW Sales.ACT_Order_Submit($order: Sales.Order)
RETURNS Boolean AS $success
BEGIN
-- validation, processing, notification...
END;
\```
The AI will read your custom skills alongside the built-in ones, learning your project’s specific patterns.
How the AI uses skills
The workflow is straightforward:
- You ask for a change: “Create a page that shows all orders”
- The AI determines which skill is relevant (in this case,
create-page.mdandoverview-pages.md) - The AI reads the skill files
- The AI writes MDL following the syntax and patterns in the skill
- The AI validates with
mxcli check - The AI executes the script
Skills act as a guardrail. Without them, AI assistants tend to guess at MDL syntax and get details wrong. With skills, the AI has a precise reference to follow, and the output is correct far more often.
Next steps
Now that you understand how skills guide AI behavior, see The MDL + AI Workflow for the complete recommended workflow from project initialization to review in Studio Pro.
The MDL + AI Workflow
This page walks through the complete recommended workflow for using mxcli with an AI assistant, from project setup to reviewing the result in Studio Pro.
The ten-step workflow
Step 1: Initialize the project
Start by running mxcli init on your Mendix project:
mxcli init /path/to/my-mendix-project
This creates the configuration files, skill documents, and dev container setup. If you use a tool other than Claude Code, specify it with --tool:
mxcli init --tool cursor /path/to/my-mendix-project
Step 2: Open in a dev container
Open the project folder in VS Code (or Cursor). VS Code will detect the .devcontainer/ directory and prompt you to reopen in a container. Click “Reopen in Container”.
The dev container provides a sandboxed environment where the AI assistant can only access your project files. It comes with mxcli, JDK, Docker, and your AI tool pre-installed.
Step 3: Start your AI assistant
Once inside the dev container, open a terminal and start your AI tool:
# Claude Code
claude
# Or use Cursor's Composer, Continue.dev's sidebar, etc.
Step 4: Describe what you want
Tell the AI what you need in plain language. Be specific about module names and mention existing elements:
“Create a Product entity in the Sales module with name, price, and description. Add an association to the existing Sales.Category entity.”
Better prompts lead to better results. A few tips:
- Name the module explicitly: “in the Sales module”
- Reference existing elements: “linked to the existing Customer entity”
- Describe the behavior, not the implementation: “a microflow that validates the email format” rather than “an IF statement that checks for @ and .”
Step 5: The AI explores your project
Before making changes, the AI uses mxcli to understand the current state of your project:
-- See what modules exist
SHOW MODULES;
-- Check what's already in the Sales module
SHOW STRUCTURE IN Sales;
-- Look at an existing entity for context
DESCRIBE ENTITY Sales.Category;
-- Search for related elements
SEARCH 'product';
This exploration step is important. The AI needs to know what entities, associations, and microflows already exist so it can write MDL that fits with your project.
Step 6: The AI writes MDL
Guided by the skill files and its exploration, the AI writes an MDL script:
/** Product catalog item */
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Product (
Name: String(200) NOT NULL,
Description: String(0),
Price: Decimal NOT NULL DEFAULT 0,
IsActive: Boolean DEFAULT true
);
/** Link products to categories */
CREATE ASSOCIATION Sales.Product_Category
FROM Sales.Product
TO Sales.Category
TYPE Reference
OWNER Default;
Step 7: The AI validates
Before executing, the AI checks for errors:
# Check syntax
./mxcli check script.mdl
# Check syntax and verify that referenced elements exist
./mxcli check script.mdl -p app.mpr --references
If there are errors, the AI fixes them and re-validates. This cycle happens automatically – you don’t need to intervene.
Step 8: The AI executes
Once validation passes, the AI runs the script against your project:
./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'"
The changes are written directly to your .mpr file.
Step 9: Deep validation
For thorough validation, the AI can run the Mendix build tools:
./mxcli docker check -p app.mpr
This uses MxBuild (the same engine Studio Pro uses) to check the project for consistency errors, missing references, and other issues that mxcli check alone might miss.
Step 10: Review in Studio Pro
Open your project in Mendix Studio Pro to review the changes visually. Check that:
- Entities appear correctly in the domain model
- Pages render as expected
- Microflow logic looks right in the visual editor
- Security settings are correct
A complete example session
Here is what a full session looks like when you ask Claude Code to build a customer management feature:
You: “Create a customer management feature in the CRM module. I need a Customer entity with name, email, phone, and status. Create an overview page with a data grid and a popup edit form. Add a microflow that validates the email before saving.”
Claude Code:
- Runs
SHOW MODULESandSHOW ENTITIES IN CRMto understand the project - Reads
generate-domain-model.md,overview-pages.md, andwrite-microflows.mdskills - Writes a script with the entity, enumeration, pages, and microflow
- Validates with
mxcli check - Executes the script
- Runs
mxcli docker checkto verify - Reports back with a summary of what was created
The entire interaction takes a few minutes. The equivalent work in Studio Pro would take considerably longer.
Tips for best results
Be specific about what exists
If you’re working with an existing project, mention the elements that are already there:
“Add a ShippingAddress field to the existing CRM.Customer entity and update the CRM.Customer_Edit page to include it.”
Let the AI explore
Don’t try to describe your entire project upfront. The AI is good at exploring. A prompt like “look at the Sales module and add a discount field to orders” is enough – the AI will figure out the entity name, existing fields, and page structure on its own.
Iterate
You don’t have to get everything right in one prompt. Start with the entity, review it, then ask for pages, then microflows. Small, focused requests tend to produce better results than one massive prompt.
Use mxcli docker check for final validation
mxcli check validates MDL syntax and references, but it doesn’t catch everything. The Mendix build tools (mxcli docker check) perform the same validation that Studio Pro does. Use it as a final gate before considering the work done.
Version control
Always work on a copy of your project or use version control. mxcli writes directly to your .mpr file. If something goes wrong, you want to be able to revert. For MPR v2 projects (Mendix 10.18+), the individual document files in mprcontents/ work well with Git.
Add custom skills for your project
If your project has specific patterns or conventions, write them down as skill files. For example, if every entity needs an audit trail (CreatedBy, CreatedAt, ChangedBy, ChangedAt), create a skill that documents this convention. The AI will follow it consistently.
MDL Basics
MDL (Mendix Definition Language) is a SQL-like language for reading and modifying Mendix application projects. It provides a text-based alternative to the visual editors in Mendix Studio Pro.
What MDL Looks Like
MDL uses familiar SQL-style syntax with Mendix-specific extensions. Here is a simple example that creates an entity with attributes:
CREATE PERSISTENT ENTITY Sales.Customer (
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
Name: String(200) NOT NULL,
Email: String(200) UNIQUE,
IsActive: Boolean DEFAULT TRUE
)
INDEX (Name);
Statement Termination
Statements are terminated with a semicolon (;) or a forward slash (/) on its own line (Oracle-style, useful for multi-line statements):
-- Semicolon terminator
CREATE MODULE OrderManagement;
-- Forward-slash terminator (useful for long statements)
CREATE PERSISTENT ENTITY Sales.Order (
OrderId: AutoNumber NOT NULL UNIQUE,
OrderDate: DateTime NOT NULL
)
INDEX (OrderDate DESC);
/
Simple commands such as HELP, EXIT, STATUS, SHOW, and DESCRIBE do not require a terminator.
Case Insensitivity
All MDL keywords are case-insensitive. The following are equivalent:
CREATE PERSISTENT ENTITY Sales.Customer ( ... );
create persistent entity Sales.Customer ( ... );
Create Persistent Entity Sales.Customer ( ... );
Identifiers (module names, entity names, attribute names) are case-sensitive and must match the Mendix model exactly.
Statement Categories
MDL statements fall into several categories:
| Category | Examples |
|---|---|
| Query | SHOW ENTITIES, DESCRIBE ENTITY, SEARCH |
| Domain Model | CREATE ENTITY, CREATE ASSOCIATION, ALTER ENTITY |
| Enumerations | CREATE ENUMERATION, ALTER ENUMERATION |
| Microflows | CREATE MICROFLOW, DROP MICROFLOW |
| Pages | CREATE PAGE, ALTER PAGE, CREATE SNIPPET |
| Security | GRANT, REVOKE, CREATE USER ROLE |
| Navigation | CREATE OR REPLACE NAVIGATION |
| Connection | CONNECT LOCAL, DISCONNECT, STATUS |
Further Reading
- Lexical Structure – keywords, literals, and tokens
- Qualified Names – how elements are referenced
- Comments – comment syntax
- Script Files – running MDL from files
Lexical Structure
This page describes the tokens that make up the MDL language: keywords, literals, and identifiers.
Keywords
MDL keywords are case-insensitive. The following are reserved keywords:
ACCESS, ACTIONS, ADD, AFTER, ALL, ALTER, AND, ANNOTATION, AS, ASC,
ASCENDING, ASSOCIATION, AUTONUMBER, BATCH, BEFORE, BEGIN, BINARY,
BOOLEAN, BOTH, BUSINESS, BY, CALL, CANCEL, CAPTION, CASCADE,
CATALOG, CHANGE, CHILD, CLOSE, COLUMN, COMBOBOX, COMMIT, CONNECT,
CONFIGURATION, CONNECTOR, CONSTANT, CONSTRAINT, CONTAINER, CREATE,
CRUD, DATAGRID, DATAVIEW, DATE, DATETIME, DECLARE, DEFAULT, DELETE,
DELETE_BEHAVIOR, DELETE_BUT_KEEP_REFERENCES, DELETE_CASCADE, DEMO,
DEPTH, DESC, DESCENDING, DESCRIBE, DIFF, DISCONNECT, DROP, ELSE,
EMPTY, END, ENTITY, ENUMERATION, ERROR, EVENT, EVENTS, EXECUTE,
EXEC, EXIT, EXPORT, EXTENDS, EXTERNAL, FALSE, FOLDER, FOOTER,
FOR, FORMAT, FROM, FULL, GALLERY, GENERATE, GRANT, HEADER, HELP,
HOME, IF, IMPORT, IN, INDEX, INFO, INSERT, INTEGER, INTO, JAVA,
KEEP_REFERENCES, LABEL, LANGUAGE, LAYOUT, LAYOUTGRID, LEVEL, LIMIT,
LINK, LIST, LISTVIEW, LOCAL, LOG, LOGIN, LONG, LOOP, MANAGE, MAP,
MATRIX, MENU, MESSAGE, MICROFLOW, MICROFLOWS, MODEL, MODIFY, MODULE,
MODULES, MOVE, NANOFLOW, NANOFLOWS, NAVIGATION, NODE, NON_PERSISTENT,
NOT, NULL, OF, ON, OR, ORACLE, OVERVIEW, OWNER, PAGE, PAGES, PARENT,
PASSWORD, PERSISTENT, POSITION, POSTGRES, PRODUCTION, PROJECT,
PROTOTYPE, QUERY, QUIT, REFERENCE, REFERENCESET, REFRESH, REMOVE,
REPLACE, REPORT, RESPONSIVE, RETRIEVE, RETURN, REVOKE, ROLE, ROLES,
ROLLBACK, ROW, SAVE, SCRIPT, SEARCH, SECURITY, SELECTION, SET, SHOW,
SNIPPET, SNIPPETS, SQL, SQLSERVER, STATUS, STRING, STRUCTURE,
TABLES, TEXTBOX, TEXTAREA, THEN, TO, TRUE, TYPE, UNIQUE, UPDATE,
USER, VALIDATION, VALUE, VIEW, VIEWS, VISIBLE, WARNING, WHERE, WIDGET,
WIDGETS, WITH, WORKFLOWS, WRITE
Most keywords work unquoted as identifiers (entity names, attribute names). Only structural keywords like CREATE, DELETE, BEGIN, END, RETURN, ENTITY, and MODULE require quoting when used as identifiers.
Literals
String Literals
String literals use single quotes:
'single quoted string'
'it''s here' -- doubled single quote to escape
The only escape sequence is '' (two single quotes) to represent a literal single quote. Backslash escaping is not supported.
Numeric Literals
42 -- Integer
3.14 -- Decimal
-100 -- Negative integer
1.5e10 -- Scientific notation
Boolean Literals
TRUE
FALSE
Quoted Identifiers
When an identifier collides with a reserved keyword, use double quotes (ANSI SQL style) or backticks (MySQL style):
"ComboBox"."CategoryTreeVE"
`Order`.`Status`
"ComboBox".CategoryTreeVE -- mixed quoting is allowed
See Qualified Names for more on identifier syntax and the Module.Name notation.
Qualified Names
MDL uses dot-separated qualified names to reference elements within a Mendix project. This naming convention ensures elements are unambiguous across modules.
Simple Identifiers
Valid identifier characters:
- Letters:
A-Z,a-z - Digits:
0-9(not as first character) - Underscore:
_
MyEntity
my_attribute
Attribute123
Qualified Name Format
The general format is Module.Element or Module.Entity.Attribute:
MyModule.Customer -- Entity in module
MyModule.OrderStatus -- Enumeration in module
MyModule.Customer.Name -- Attribute in entity
Two-Part Names
Most elements use a two-part name: Module.ElementName.
Sales.Customer -- entity
Sales.OrderStatus -- enumeration
Sales.ACT_CreateOrder -- microflow
Sales.Customer_Edit -- page
Sales.Order_Customer -- association
Three-Part Names
Attribute references use three parts: Module.Entity.Attribute.
Sales.Customer.Name -- attribute on entity
Sales.Order.TotalAmount -- attribute on entity
Quoting Rules
When a name segment is a reserved keyword, wrap it in double quotes or backticks:
"ComboBox"."CategoryTreeVE"
`Order`.`Status`
Mixed quoting is allowed – you only need to quote the segments that conflict:
"ComboBox".CategoryTreeVE
Sales."Order"
Identifiers that contain only letters, digits, and underscores (and do not start with a digit) never need quoting.
Usage in Statements
Qualified names appear throughout MDL:
-- Entity creation
CREATE PERSISTENT ENTITY Sales.Customer ( ... );
-- Association referencing two entities
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference;
-- Enumeration reference in an attribute type
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Active'
-- Microflow call
$Result = CALL MICROFLOW Sales.ACT_ProcessOrder ($Order = $Order);
See Also
- Lexical Structure – keywords and quoting
- Entities – entity qualified names
- Associations – association naming conventions
Comments and Documentation
MDL supports three comment styles for annotating scripts and attaching documentation to model elements.
Single-Line Comments
Use -- (SQL style) or // (C style) for single-line comments:
-- This is a single-line comment
SHOW MODULES
// This is also a single-line comment
SHOW ENTITIES IN Sales
Everything after the comment marker to the end of the line is ignored.
Multi-Line Comments
Use /* ... */ for comments that span multiple lines:
/* This comment spans
multiple lines and is useful
for longer explanations */
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200) NOT NULL
);
Documentation Comments
Use /** ... */ to attach documentation to model elements. Documentation comments are stored in the Mendix model and appear in Studio Pro:
/** Customer entity stores master customer data.
* Used by the Sales and Support modules.
*/
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
/** Unique customer identifier, auto-generated */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Full legal name of the customer */
Name: String(200) NOT NULL,
/** Primary contact email address */
Email: String(200) UNIQUE
);
Documentation comments can be placed before:
- Entity definitions (becomes entity documentation)
- Attribute definitions (becomes attribute documentation)
- Enumeration definitions (becomes enumeration documentation)
- Association definitions (becomes association documentation)
Updating Documentation
You can also set documentation on existing entities with ALTER ENTITY:
ALTER ENTITY Sales.Customer
SET DOCUMENTATION 'Customer master data for the Sales module';
Script Files
MDL statements can be saved in .mdl files and executed as scripts. This is the primary way to automate Mendix model changes.
File Format
- Extension:
.mdl - Encoding: UTF-8
- Statement termination: Semicolons (
;) or forward slash (/) on its own line
A typical script file:
-- setup_domain_model.mdl
-- Creates the Sales domain model
CREATE ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending',
Confirmed 'Confirmed',
Shipped 'Shipped'
);
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200) NOT NULL,
Email: String(200) UNIQUE
);
CREATE PERSISTENT ENTITY Sales.Order (
OrderDate: DateTime NOT NULL,
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
);
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference;
Executing Scripts
From the Command Line
Use mxcli exec to run a script against a project:
mxcli exec setup_domain_model.mdl -p /path/to/app.mpr
From the REPL
Use EXECUTE SCRIPT inside an interactive session:
CONNECT LOCAL '/path/to/app.mpr';
EXECUTE SCRIPT './scripts/setup_domain_model.mdl';
Syntax Checking
You can validate a script without connecting to a project:
# Syntax only
mxcli check script.mdl
# Syntax + reference validation (requires project)
mxcli check script.mdl -p app.mpr --references
Syntax checking catches parse errors, unknown keywords, and common anti-patterns before execution.
Comments in Scripts
Scripts support the same comment syntax as interactive MDL:
-- Single-line comment
/* Multi-line comment */
/** Documentation comment (attached to next element) */
Data Types
MDL has a type system that maps to the Mendix metamodel attribute types. Every attribute in an entity definition has a type that determines how values are stored and validated.
Type Categories
| Category | Types |
|---|---|
| Text | String, String(n), HashedString |
| Numeric | Integer, Long, Decimal, AutoNumber |
| Logical | Boolean |
| Temporal | DateTime, Date |
| Binary | Binary |
| Reference | Enumeration(Module.Name) |
Quick Example
CREATE PERSISTENT ENTITY Demo.AllTypes (
Id: AutoNumber NOT NULL UNIQUE DEFAULT 1,
Code: String(10) NOT NULL UNIQUE,
Name: String(200) NOT NULL,
Description: String(unlimited),
Counter: Integer DEFAULT 0,
BigNumber: Long,
Amount: Decimal DEFAULT 0.00,
IsActive: Boolean DEFAULT TRUE,
CreatedAt: DateTime,
BirthDate: Date,
Attachment: Binary,
Status: Enumeration(Demo.Status) DEFAULT 'Active'
);
No Implicit Conversions
MDL does not perform implicit type conversions. Types must match exactly when assigning defaults or modifying attributes.
Compatible type changes (when using CREATE OR MODIFY):
String(100)toString(200)– increasing lengthIntegertoLong– widening numeric type
Incompatible type changes:
StringtoIntegerBooleantoString- Any type to
AutoNumber(if data exists)
Further Reading
- Primitive Types – detailed reference for each type
- Constraints – NOT NULL, UNIQUE, DEFAULT
- Type Mapping – MDL to Mendix to database mapping
- Enumerations – enumeration type definitions
Primitive Types
This page documents each primitive type available for entity attributes in MDL.
String
Variable-length text data.
String -- Default length (200)
String(n) -- Specific length (1 to unlimited)
String(unlimited) -- No length limit
Examples:
Name: String(200)
Description: String(unlimited)
Code: String(10)
Default value format:
Name: String(200) DEFAULT 'Unknown'
Code: String(10) DEFAULT ''
Integer
32-bit signed integer.
Integer
Range: -2,147,483,648 to 2,147,483,647
Quantity: Integer
Age: Integer DEFAULT 0
Priority: Integer NOT NULL DEFAULT 1
Long
64-bit signed integer.
Long
Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
FileSize: Long
TotalCount: Long DEFAULT 0
Decimal
High-precision decimal number with up to 20 digits total.
Decimal
Price: Decimal
Amount: Decimal DEFAULT 0
TaxRate: Decimal DEFAULT 0.21
Boolean
True/false value. Boolean attributes must have a DEFAULT value (enforced by Mendix Studio Pro).
Boolean DEFAULT TRUE
Boolean DEFAULT FALSE
IsActive: Boolean DEFAULT TRUE
Enabled: Boolean DEFAULT TRUE
Deleted: Boolean DEFAULT FALSE
DateTime
Date and time stored as a UTC timestamp.
DateTime
CreatedAt: DateTime
ModifiedAt: DateTime
ScheduledFor: DateTime
DateTime values include both date and time components. For date-only display, use Date instead.
Date
Date only (no time component). Internally stored as DateTime in Mendix, but the UI only shows the date portion.
Date
BirthDate: Date
ExpiryDate: Date
AutoNumber
Auto-incrementing integer, typically used for human-readable identifiers. The DEFAULT value specifies the starting number.
AutoNumber
OrderId: AutoNumber NOT NULL UNIQUE DEFAULT 1
CustomerId: AutoNumber
AutoNumber attributes automatically receive the next value on object creation. They are typically combined with NOT NULL and UNIQUE constraints.
Binary
Binary data for files, images, and other non-text content.
Binary
ProfileImage: Binary
Document: Binary
Thumbnail: Binary
File metadata (name, size, MIME type) is stored separately by Mendix. Maximum size is configurable per attribute.
HashedString
Securely hashed string, used for password storage. Values are one-way hashed and cannot be retrieved – comparison is done by hashing the input and comparing hashes.
HashedString
Password: HashedString
Enumeration Reference
References an enumeration type defined with CREATE ENUMERATION. See Enumerations for details.
Enumeration(<Module.EnumerationName>)
Status: Enumeration(Sales.OrderStatus)
Priority: Enumeration(Core.Priority) DEFAULT Core.Priority.Normal
Type: Enumeration(MyModule.ItemType) NOT NULL
Default value format:
-- Fully qualified (preferred)
DEFAULT Module.EnumName.ValueName
-- Legacy string literal form
DEFAULT 'ValueName'
See Also
- Data Types – type system overview
- Constraints – NOT NULL, UNIQUE, DEFAULT
- Type Mapping – MDL to Mendix to database mapping
Constraints
Constraints restrict the values an attribute can hold. They are specified after the type in an attribute definition.
Syntax
<name>: <type> [NOT NULL [ERROR '<message>']] [UNIQUE [ERROR '<message>']] [DEFAULT <value>]
Constraints must appear in this order:
NOT NULL(optionally withERROR)UNIQUE(optionally withERROR)DEFAULT
NOT NULL
Marks the attribute as required. The value cannot be empty.
Name: String(200) NOT NULL
With a custom error message displayed to the user:
Name: String(200) NOT NULL ERROR 'Name is required'
UNIQUE
Enforces that the value must be unique across all objects of the entity.
Email: String(200) UNIQUE
With a custom error message:
Email: String(200) UNIQUE ERROR 'Email already exists'
DEFAULT
Sets the initial value when a new object is created.
Count: Integer DEFAULT 0
IsActive: Boolean DEFAULT TRUE
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
Name: String(200) DEFAULT 'Unknown'
Default value syntax varies by type:
| Type | Default Syntax | Examples |
|---|---|---|
| String | DEFAULT 'value' | DEFAULT '', DEFAULT 'Unknown' |
| Integer | DEFAULT n | DEFAULT 0, DEFAULT -1 |
| Long | DEFAULT n | DEFAULT 0 |
| Decimal | DEFAULT n.n | DEFAULT 0, DEFAULT 0.00, DEFAULT 99.99 |
| Boolean | DEFAULT TRUE/FALSE | DEFAULT TRUE, DEFAULT FALSE |
| AutoNumber | DEFAULT n | DEFAULT 1 (starting value) |
| Enumeration | DEFAULT Module.Enum.Value | DEFAULT Shop.Status.Active, DEFAULT 'Pending' |
Omitting the DEFAULT clause means no default value is set:
OptionalField: String(200)
Combining Constraints
All three constraints can be used together:
Email: String(200) NOT NULL ERROR 'Email is required'
UNIQUE ERROR 'Email already exists'
DEFAULT ''
More examples:
CREATE PERSISTENT ENTITY Sales.Product (
-- Required only
Name: String(200) NOT NULL,
-- Required with custom error
SKU: String(50) NOT NULL ERROR 'SKU is required for all products',
-- Unique only
Barcode: String(50) UNIQUE,
-- Required and unique with custom errors
ProductCode: String(20) NOT NULL ERROR 'Product code required'
UNIQUE ERROR 'Product code must be unique',
-- Default only
Quantity: Integer DEFAULT 0,
-- No constraints
Description: String(unlimited)
);
Validation Rule Mapping
Under the hood, constraints map to Mendix validation rules:
| MDL Constraint | Mendix Validation Rule |
|---|---|
NOT NULL | DomainModels$RequiredRuleInfo |
UNIQUE | DomainModels$UniqueRuleInfo |
See Also
- Primitive Types – type reference
- Attributes – full attribute definition syntax
- ALTER ENTITY – modifying constraints on existing entities
Enumerations
Enumerations define a fixed set of named values. They are used as attribute types to restrict a field to a specific list of options.
CREATE ENUMERATION
[/** <documentation> */]
CREATE ENUMERATION <Module>.<Name> (
<value-name> '<caption>' [, ...]
)
Each value has an identifier (used in code) and a caption (displayed in the UI):
/** Order status enumeration */
CREATE ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending Approval',
Approved 'Approved',
Shipped 'Shipped',
Delivered 'Delivered',
Cancelled 'Cancelled'
);
Using Enumerations in Attributes
Reference an enumeration type in an attribute definition with Enumeration(Module.Name):
CREATE PERSISTENT ENTITY Sales.Order (
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft',
Priority: Enumeration(Core.Priority) NOT NULL
);
The default value is the name of the enumeration value (not the caption). The fully qualified form is preferred:
-- Fully qualified (preferred)
Status: Enumeration(Sales.OrderStatus) DEFAULT Sales.OrderStatus.Draft
-- Legacy string literal form
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
ALTER ENUMERATION
Add or remove values from an existing enumeration:
-- Add a new value
ALTER ENUMERATION Sales.OrderStatus
ADD VALUE OnHold 'On Hold';
-- Remove a value
ALTER ENUMERATION Sales.OrderStatus
REMOVE VALUE Draft;
DROP ENUMERATION
Remove an enumeration entirely:
DROP ENUMERATION Sales.OrderStatus;
Dropping an enumeration that is still referenced by entity attributes will cause errors in Studio Pro.
See Also
- Primitive Types – all attribute types including enumeration references
- Data Types – type system overview
- Entities – using enumerations in entity definitions
Type Mapping
This page shows how MDL types correspond to Mendix internal types (BSON storage) and the Go SDK types used by modelsdk-go.
MDL to Backend Type Mapping
| MDL Type | BSON $Type | Go SDK Type |
|---|---|---|
String | DomainModels$StringAttributeType | *StringAttributeType |
String(n) | DomainModels$StringAttributeType + Length | *StringAttributeType{Length: n} |
Integer | DomainModels$IntegerAttributeType | *IntegerAttributeType |
Long | DomainModels$LongAttributeType | *LongAttributeType |
Decimal | DomainModels$DecimalAttributeType | *DecimalAttributeType |
Boolean | DomainModels$BooleanAttributeType | *BooleanAttributeType |
DateTime | DomainModels$DateTimeAttributeType | *DateTimeAttributeType |
Date | DomainModels$DateTimeAttributeType | *DateTimeAttributeType |
AutoNumber | DomainModels$AutoNumberAttributeType | *AutoNumberAttributeType |
Binary | DomainModels$BinaryAttributeType | *BinaryAttributeType |
Enumeration | DomainModels$EnumerationAttributeType | *EnumerationAttributeType |
HashedString | DomainModels$HashedStringAttributeType | *HashedStringAttributeType |
Note that Date and DateTime both map to the same underlying BSON type. The distinction is handled at the UI layer.
Default Value Mapping
Default values are stored in BSON as StoredValue structures:
| MDL Default | BSON Structure |
|---|---|
DEFAULT 'text' | Value: {$Type: "DomainModels$StoredValue", DefaultValue: "text"} |
DEFAULT 123 | Value: {$Type: "DomainModels$StoredValue", DefaultValue: "123"} |
DEFAULT TRUE | Value: {$Type: "DomainModels$StoredValue", DefaultValue: "true"} |
| (calculated) | Value: {$Type: "DomainModels$CalculatedValue", Microflow: <id>} |
All default values are serialized as strings in the BSON storage, regardless of the attribute type.
See Also
- Primitive Types – MDL type syntax and usage
- Data Types – type system overview
Domain Model
The domain model is the data layer of a Mendix application. It defines the entities (data tables), their attributes (columns), relationships between entities (associations), and inheritance hierarchies (generalizations).
Core Concepts
| Concept | MDL Statement | Description |
|---|---|---|
| Entity | CREATE ENTITY | A data structure, similar to a database table |
| Attribute | Defined inside entity | A field on an entity, similar to a column |
| Association | CREATE ASSOCIATION | A relationship between two entities |
| Generalization | EXTENDS | Inheritance – one entity extends another |
| Index | INDEX (...) | Performance optimization for queries |
| Enumeration | CREATE ENUMERATION | A fixed set of named values for an attribute |
Module Scope
Every domain model element belongs to a module. Elements are always referenced by their qualified name in Module.Element format:
Sales.Customer -- entity
Sales.OrderStatus -- enumeration
Sales.Order_Customer -- association
Complete Example
This example creates a small Sales domain model with related entities, an enumeration, and an association:
-- Enumeration for order statuses
CREATE ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending',
Confirmed 'Confirmed',
Shipped 'Shipped',
Delivered 'Delivered',
Cancelled 'Cancelled'
);
-- Customer entity
/** Customer master data */
@Position(100, 100)
CREATE PERSISTENT ENTITY Sales.Customer (
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
Name: String(200) NOT NULL ERROR 'Customer name is required',
Email: String(200) UNIQUE ERROR 'Email already registered',
Phone: String(50),
IsActive: Boolean DEFAULT TRUE,
CreatedAt: DateTime
)
INDEX (Name)
INDEX (Email);
-- Order entity
/** Sales order */
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Order (
OrderId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
OrderNumber: String(50) NOT NULL UNIQUE,
OrderDate: DateTime NOT NULL,
TotalAmount: Decimal DEFAULT 0,
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft',
Notes: String(unlimited)
)
INDEX (OrderNumber)
INDEX (OrderDate DESC);
-- Association: Order belongs to Customer
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;
Further Reading
- Entities – entity types and CREATE ENTITY syntax
- Attributes – attribute definitions and validation
- Associations – relationships between entities
- Generalization – entity inheritance
- Indexes – index creation
- Enumerations – enumeration types
- ALTER ENTITY – modifying existing entities
Entities
Entities are the primary data structures in a Mendix domain model. Each entity corresponds to a database table (for persistent entities) or an in-memory object (for non-persistent entities).
Entity Types
| Type | MDL Keyword | Description |
|---|---|---|
| Persistent | PERSISTENT | Stored in the database with a corresponding table |
| Non-Persistent | NON-PERSISTENT | In-memory only, scoped to the user session |
| View | VIEW | Based on an OQL query, read-only |
| External | EXTERNAL | From an external data source (OData, etc.) |
CREATE ENTITY
[/** <documentation> */]
[@Position(<x>, <y>)]
CREATE [OR MODIFY] <entity-type> ENTITY <Module>.<Name> (
<attribute-definitions>
)
[INDEX (<column-list>)]
[;|/]
Persistent Entity
The most common type. Data is stored in the application database:
/** Customer entity */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
Name: String(200) NOT NULL ERROR 'Name is required',
Email: String(200) UNIQUE ERROR 'Email must be unique',
Balance: Decimal DEFAULT 0,
IsActive: Boolean DEFAULT TRUE,
CreatedDate: DateTime,
Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name)
INDEX (Email);
Non-Persistent Entity
Used for helper objects, filter parameters, and UI state that does not need database storage:
CREATE NON-PERSISTENT ENTITY Sales.CustomerFilter (
SearchName: String(200),
MinBalance: Decimal,
MaxBalance: Decimal
);
View Entity
Defined by an OQL query. View entities are read-only:
CREATE VIEW ENTITY Reports.CustomerSummary (
CustomerName: String,
TotalOrders: Integer,
TotalAmount: Decimal
) AS
SELECT
c.Name AS CustomerName,
COUNT(o.OrderId) AS TotalOrders,
SUM(o.Amount) AS TotalAmount
FROM Sales.Customer c
LEFT JOIN Sales.Order o ON o.Customer = c
GROUP BY c.Name;
CREATE OR MODIFY
Creates the entity if it does not exist, or updates it if it does. New attributes are added; existing attributes are preserved:
CREATE OR MODIFY PERSISTENT ENTITY Sales.Customer (
CustomerId: AutoNumber NOT NULL UNIQUE,
Name: String(200) NOT NULL,
Email: String(200),
Phone: String(50) -- new attribute added on modify
);
Annotations
@Position
Controls where the entity appears in the domain model diagram:
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer ( ... );
Documentation
A /** ... */ comment before the entity becomes its documentation in Studio Pro:
/** Customer master data.
* Stores both active and inactive customers.
*/
CREATE PERSISTENT ENTITY Sales.Customer ( ... );
DROP ENTITY
Removes an entity from the domain model:
DROP ENTITY Sales.Customer;
See Also
- Attributes – attribute definitions within entities
- Indexes – adding indexes to entities
- Generalization – entity inheritance with EXTENDS
- ALTER ENTITY – modifying existing entities
- Associations – relationships between entities
Attributes and Validation Rules
Attributes define the fields on an entity. Each attribute has a name, a type, and optional constraints.
Attribute Syntax
[/** <documentation> */]
<name>: <type> [NOT NULL [ERROR '<message>']] [UNIQUE [ERROR '<message>']] [DEFAULT <value>]
| Component | Description |
|---|---|
| Name | Attribute identifier (follows identifier rules) |
| Type | One of the primitive types or an enumeration reference |
NOT NULL | Value is required |
UNIQUE | Value must be unique across all objects |
DEFAULT | Initial value on object creation |
/** ... */ | Documentation comment |
Attribute Ordering
Attributes are listed in order, separated by commas. The last attribute has no trailing comma:
CREATE PERSISTENT ENTITY Module.Entity (
FirstAttr: String(200), -- comma after
SecondAttr: Integer, -- comma after
LastAttr: Boolean DEFAULT FALSE -- no comma
);
Validation Rules
Validation rules are expressed as attribute constraints. When validation fails, Mendix displays the error message (if provided) or a default message.
| Validation | MDL Syntax | Description |
|---|---|---|
| Required | NOT NULL | Attribute must have a value |
| Required with message | NOT NULL ERROR 'message' | Custom error message on empty |
| Unique | UNIQUE | Value must be unique across all objects |
| Unique with message | UNIQUE ERROR 'message' | Custom error message on duplicate |
Example with Validation
CREATE PERSISTENT ENTITY Sales.Product (
-- Required only
Name: String(200) NOT NULL,
-- Required with custom error
SKU: String(50) NOT NULL ERROR 'SKU is required for all products',
-- Unique only
Barcode: String(50) UNIQUE,
-- Required and unique with custom errors
ProductCode: String(20) NOT NULL ERROR 'Product code required'
UNIQUE ERROR 'Product code must be unique',
-- Optional field (no validation)
Description: String(unlimited)
);
Documentation Comments
Attach documentation to individual attributes with /** ... */:
CREATE PERSISTENT ENTITY Sales.Customer (
/** Unique customer identifier, auto-generated */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Full legal name of the customer */
Name: String(200) NOT NULL,
/** Primary contact email address */
Email: String(200) UNIQUE
);
CALCULATED Attributes
An attribute can be marked as calculated, meaning its value is computed by a microflow rather than stored:
FullName: String(400) CALCULATED
Calculated attributes use a CALCULATED BY clause to specify the source microflow. See the MDL quick reference for details.
See Also
- Primitive Types – all available attribute types
- Constraints – NOT NULL, UNIQUE, DEFAULT in detail
- Entities – entity definitions that contain attributes
- ALTER ENTITY – adding and modifying attributes on existing entities
Associations
Associations define relationships between entities. They determine how objects reference each other and control behavior on deletion.
Association Types
| Type | MDL Keyword | Cardinality | Description |
|---|---|---|---|
| Reference | Reference | Many-to-One | Many objects on the FROM side reference one object on the TO side |
| ReferenceSet | ReferenceSet | Many-to-Many | Objects on both sides can reference multiple objects |
CREATE ASSOCIATION
[/** <documentation> */]
CREATE ASSOCIATION <Module>.<Name>
FROM <ParentEntity>
TO <ChildEntity>
TYPE <Reference|ReferenceSet>
[OWNER <Default|Both|Parent|Child>]
[DELETE_BEHAVIOR <behavior>]
Reference (Many-to-One)
A Reference association means each object on the FROM side can reference one object on the TO side. Multiple FROM objects can reference the same TO object.
/** Order belongs to Customer (many-to-one) */
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;
ReferenceSet (Many-to-Many)
A ReferenceSet association allows multiple objects on both sides to reference each other:
/** Order has many Products (many-to-many) */
CREATE ASSOCIATION Sales.Order_Product
FROM Sales.Order
TO Sales.Product
TYPE ReferenceSet
OWNER Both;
Owner Options
The owner determines which side of the association can modify the reference.
| Owner | Description |
|---|---|
Default | Child (FROM side) owns the association |
Both | Both sides can modify the association |
Parent | Only parent (TO side) can modify |
Child | Only child (FROM side) can modify |
Delete Behavior
Controls what happens when an associated object is deleted.
| Behavior | MDL Keyword | Description |
|---|---|---|
| Keep references | DELETE_BUT_KEEP_REFERENCES | Delete the object, set references to null |
| Cascade | DELETE_CASCADE | Delete associated objects as well |
/** Invoice must be deleted with Order */
CREATE ASSOCIATION Sales.Order_Invoice
FROM Sales.Order
TO Sales.Invoice
TYPE Reference
DELETE_BEHAVIOR DELETE_CASCADE;
Naming Convention
Association names typically follow the pattern Module.FromEntity_ToEntity:
Sales.Order_Customer -- Order references Customer
Sales.Order_Product -- Order references Product
Sales.Order_Invoice -- Order references Invoice
DROP ASSOCIATION
Remove an association:
DROP ASSOCIATION Sales.Order_Customer;
See Also
- Domain Model – overview of domain model concepts
- Entities – the entities that associations connect
- Generalization – inheritance (a different kind of relationship)
Generalization
Generalization (inheritance) allows an entity to extend another entity, inheriting all of its attributes and associations. The child entity can add its own attributes on top.
Syntax
CREATE PERSISTENT ENTITY <Module>.<Name>
EXTENDS <ParentEntity>
(
<additional-attributes>
);
Both EXTENDS (preferred) and GENERALIZATION (legacy) keywords are supported.
Examples
Extending System.User
The most common use of generalization is creating an application-specific user entity:
/** Employee extends User with additional fields */
CREATE PERSISTENT ENTITY HR.Employee EXTENDS System.User (
EmployeeNumber: String(20) NOT NULL UNIQUE,
Department: String(100),
HireDate: Date
);
The HR.Employee entity inherits all attributes from System.User (Name, Password, etc.) and adds EmployeeNumber, Department, and HireDate.
Extending System.Image
For entities that store images:
/** Product photo entity */
CREATE PERSISTENT ENTITY Catalog.ProductPhoto EXTENDS System.Image (
Caption: String(200),
SortOrder: Integer DEFAULT 0
);
Extending System.FileDocument
For entities that store file attachments:
/** File attachment entity */
CREATE PERSISTENT ENTITY Docs.Attachment EXTENDS System.FileDocument (
Description: String(500)
);
Common System Generalizations
| Parent Entity | Purpose |
|---|---|
System.User | User accounts with authentication |
System.FileDocument | File storage (name, size, content) |
System.Image | Image storage (extends FileDocument with dimensions) |
Inheritance Rules
- A persistent entity can extend another persistent entity
- The child entity inherits all attributes and associations of the parent
- The child entity can add new attributes but cannot remove inherited ones
- Associations to the parent entity also apply to child objects
- Database queries on the parent entity include child objects
See Also
- Entities – entity types and CREATE ENTITY syntax
- Associations – relationships between entities
- Domain Model – domain model overview
Indexes
Indexes improve query performance for frequently searched or sorted attributes. They are defined after the attribute list in an entity definition.
Syntax
INDEX (<column> [ASC|DESC] [, <column> [ASC|DESC] ...])
Indexes are placed after the closing parenthesis of the attribute list:
CREATE PERSISTENT ENTITY Sales.Order (
OrderId: AutoNumber NOT NULL UNIQUE,
OrderNumber: String(50) NOT NULL,
CustomerId: Long,
OrderDate: DateTime,
Status: Enumeration(Sales.OrderStatus)
)
-- Single column index
INDEX (OrderNumber)
-- Composite index with sort direction
INDEX (CustomerId, OrderDate DESC)
-- Another single column index
INDEX (Status);
Sort Direction
Each column in an index can specify a sort direction:
| Direction | Keyword | Default |
|---|---|---|
| Ascending | ASC | Yes (default) |
| Descending | DESC | No |
INDEX (Name) -- ascending (default)
INDEX (Name ASC) -- explicit ascending
INDEX (CreatedAt DESC) -- descending
INDEX (Name, CreatedAt DESC) -- mixed: Name ascending, CreatedAt descending
Adding and Removing Indexes
Use ALTER ENTITY to add or remove indexes on existing entities:
-- Add an index
ALTER ENTITY Sales.Customer
ADD INDEX (Email);
-- Remove an index
ALTER ENTITY Sales.Customer
DROP INDEX (Email);
Guidelines
- Primary lookups – index columns used in WHERE clauses
- Foreign keys – index columns used in association joins
- Sorting – index columns used in ORDER BY clauses
- Composite order – put high-selectivity columns first in composite indexes
See Also
- Entities – entity definitions that contain indexes
- ALTER ENTITY – adding/removing indexes on existing entities
ALTER ENTITY
ALTER ENTITY modifies an existing entity’s attributes, indexes, or documentation without recreating it. This is useful for incremental changes to entities that already contain data.
ADD Attributes
Add one or more new attributes:
ALTER ENTITY Sales.Customer
ADD (Phone: String(50), Notes: String(unlimited));
New attributes support the same constraints as in CREATE ENTITY:
ALTER ENTITY Sales.Customer
ADD (
LoyaltyPoints: Integer DEFAULT 0,
MemberSince: DateTime NOT NULL
);
DROP Attributes
Remove one or more attributes:
ALTER ENTITY Sales.Customer
DROP (Notes);
Multiple attributes can be dropped at once:
ALTER ENTITY Sales.Customer
DROP (Notes, TempField, OldStatus);
MODIFY Attributes
Change the type or constraints of existing attributes:
ALTER ENTITY Sales.Customer
MODIFY (Name: String(400) NOT NULL);
RENAME Attributes
Rename an attribute:
ALTER ENTITY Sales.Customer
RENAME Phone TO PhoneNumber;
ADD INDEX
Add an index to the entity:
ALTER ENTITY Sales.Customer
ADD INDEX (Email);
Composite indexes:
ALTER ENTITY Sales.Customer
ADD INDEX (Name, CreatedAt DESC);
DROP INDEX
Remove an index:
ALTER ENTITY Sales.Customer
DROP INDEX (Email);
SET DOCUMENTATION
Update the entity’s documentation text:
ALTER ENTITY Sales.Customer
SET DOCUMENTATION 'Customer master data for the Sales module';
Syntax Summary
ALTER ENTITY <Module>.<Entity>
ADD (<attribute-definition> [, ...])
ALTER ENTITY <Module>.<Entity>
DROP (<attribute-name> [, ...])
ALTER ENTITY <Module>.<Entity>
MODIFY (<attribute-definition> [, ...])
ALTER ENTITY <Module>.<Entity>
RENAME <old-name> TO <new-name>
ALTER ENTITY <Module>.<Entity>
ADD INDEX (<column-list>)
ALTER ENTITY <Module>.<Entity>
DROP INDEX (<column-list>)
ALTER ENTITY <Module>.<Entity>
SET DOCUMENTATION '<text>'
See Also
- Entities – CREATE ENTITY syntax
- Attributes – attribute definition format
- Indexes – index creation and management
- Constraints – NOT NULL, UNIQUE, DEFAULT
Microflows and Nanoflows
Microflows and nanoflows are the primary logic constructs in Mendix applications. They define executable sequences of activities – retrieving data, creating objects, calling services, showing pages, and more. In MDL, you create and manage them with declarative SQL-like syntax.
What is a Microflow?
A microflow is a server-side logic flow. It executes on the Mendix runtime, has access to the database, can call external services, and supports transactions with error handling. Microflows are the workhorse of Mendix application logic.
Typical uses:
- CRUD operations (create, read, update, delete objects)
- Validation logic before saving
- Calling external web services or Java actions
- Batch processing and data transformations
- Scheduled event handlers
What is a Nanoflow?
A nanoflow is a client-side logic flow. It executes in the user’s browser (or on mobile devices), providing fast response times without server round-trips. Nanoflows have a more limited set of available activities – they cannot access the database directly or use Java actions.
Typical uses:
- Client-side validation
- Showing/closing pages
- Changing objects already in memory
- Calling nanoflows or microflows
Basic Syntax
Both microflows and nanoflows follow the same structural pattern in MDL:
CREATE MICROFLOW Module.ActionName
FOLDER 'OptionalFolder'
BEGIN
-- parameters, variables, activities, return
END;
CREATE NANOFLOW Module.ClientAction
BEGIN
-- activities (client-side subset)
END;
SHOW and DESCRIBE
List microflows and nanoflows in the project:
SHOW MICROFLOWS
SHOW MICROFLOWS IN MyModule
SHOW NANOFLOWS
SHOW NANOFLOWS IN MyModule
View the full MDL definition of an existing microflow or nanoflow (round-trippable output):
DESCRIBE MICROFLOW MyModule.ACT_CreateOrder
DESCRIBE NANOFLOW MyModule.NAV_ShowDetails
DROP
Remove a microflow or nanoflow:
DROP MICROFLOW MyModule.ACT_CreateOrder;
DROP NANOFLOW MyModule.NAV_ShowDetails;
OR REPLACE
Use CREATE OR REPLACE to overwrite an existing microflow or nanoflow if it already exists, or create it if it does not:
CREATE OR REPLACE MICROFLOW MyModule.ACT_CreateOrder
BEGIN
-- updated logic
END;
Folder Organization
Place microflows and nanoflows into folders for project organization:
CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
-- ...
END;
Move an existing microflow to a different folder:
MOVE MICROFLOW Sales.ACT_CreateOrder TO FOLDER 'Orders/Actions';
MOVE NANOFLOW Sales.NAV_ShowDetail TO FOLDER 'Navigation';
Nested folders use / as the separator. Missing folders are created automatically.
Quick Example
CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Status = 'Draft'
);
COMMIT $Order;
SHOW PAGE Sales.Order_Edit ($Order = $Order);
RETURN $Order;
END;
This microflow creates a new Order object with default values, commits it to the database, opens the edit page, and returns the new order.
Next Steps
- Structure – parameters, variables, return types, and the full
CREATE MICROFLOWsyntax - Activity Types – all available activities (RETRIEVE, CREATE, CHANGE, COMMIT, DELETE, CALL, LOG, etc.)
- Control Flow – IF/ELSE, LOOP, WHILE, error handling
- Expressions – expression syntax for conditions and calculations
- Nanoflows vs Microflows – differences and nanoflow-specific syntax
- Common Patterns – CRUD, validation, batch processing, and anti-patterns to avoid
Structure
This page covers the structural elements of a microflow definition: the CREATE MICROFLOW syntax, parameters, variable declarations, return values, and annotations.
Full CREATE MICROFLOW Syntax
CREATE [OR REPLACE] MICROFLOW <Module.Name>
[FOLDER '<path>']
BEGIN
[<declarations>]
[<activities>]
[RETURN <value>;]
END;
The OR REPLACE modifier overwrites an existing microflow of the same name. The FOLDER clause organizes the microflow within the module’s folder structure.
Parameters
Parameters are declared with DECLARE at the top of the BEGIN...END block. They define the inputs to the microflow.
Primitive Parameters
DECLARE $Name String;
DECLARE $Count Integer;
DECLARE $IsActive Boolean;
DECLARE $Amount Decimal;
DECLARE $StartDate DateTime;
Supported primitive types: String, Integer, Long, Boolean, Decimal, DateTime.
Entity Parameters
Entity parameters receive a single object:
DECLARE $Customer MyModule.Customer;
Important: Do not use
= emptyorASwith entity declarations. The correct syntax is simplyDECLARE $Var Module.Entity;.
List Parameters
List parameters receive a list of objects:
DECLARE $Orders List of Sales.Order = empty;
The = empty initializer creates an empty list. This is required for list declarations.
Parameter vs. Local Variable
In MDL, all DECLARE statements at the top of a microflow are treated as parameters. Variables created by activities (such as $Var = CREATE ... or RETRIEVE $Var ...) are local variables. If you need a local variable with a default value, use DECLARE followed by SET:
DECLARE $Counter Integer = 0;
SET $Counter = 10;
Variables and Assignment
Variable Declaration
DECLARE $Message String = 'Hello';
DECLARE $Total Decimal = 0;
DECLARE $Found Boolean = false;
Assignment with SET
Change the value of an already-declared variable:
SET $Counter = $Counter + 1;
SET $FullName = $FirstName + ' ' + $LastName;
SET $IsValid = $Amount > 0;
The variable must be declared before it can be assigned.
Variables from Activities
Activities like CREATE and RETRIEVE produce result variables:
$Order = CREATE Sales.Order (Status = 'New');
RETRIEVE $Customer FROM Sales.Customer WHERE Email = $Email LIMIT 1;
$Result = CALL MICROFLOW Sales.CalculateTotal (Order = $Order);
Return Values
Every flow path should end with a RETURN statement. The return type is inferred from the returned value.
Returning a Primitive
RETURN true;
RETURN $Total;
RETURN 'Success';
Returning an Object
RETURN $Order;
Returning a List
RETURN $FilteredOrders;
Returning Nothing
If the microflow returns nothing (void), you can omit the RETURN or use:
RETURN;
Annotations
Annotations are metadata decorators placed before an activity. They control visual layout and documentation in Mendix Studio Pro.
Position
Set the canvas position of the next activity:
@position(200, 100)
$Order = CREATE Sales.Order (Status = 'New');
Caption
Set a custom caption displayed on the activity in the canvas:
@caption 'Create new order'
$Order = CREATE Sales.Order (Status = 'New');
Color
Set the background color of the activity:
@color Green
COMMIT $Order;
Annotation (Visual Note)
Attach a visual annotation note to the next activity:
@annotation 'This step validates the input before saving'
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Email is required';
Combining Annotations
Multiple annotations can be stacked before a single activity:
@position(300, 200)
@caption 'Validate and save'
@color Green
@annotation 'Final step: commit the validated order'
COMMIT $Order WITH EVENTS;
Complete Example
CREATE MICROFLOW Sales.ACT_ProcessOrder
FOLDER 'Orders/Processing'
BEGIN
-- Parameters
DECLARE $Order Sales.Order;
DECLARE $ApplyDiscount Boolean;
-- Local variable
DECLARE $Total Decimal = 0;
-- Retrieve order lines
RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
-- Calculate total
@caption 'Calculate total'
$Total = CALL MICROFLOW Sales.SUB_CalculateTotal (
OrderLines = $Lines
);
-- Apply discount if requested
IF $ApplyDiscount THEN
SET $Total = $Total * 0.9;
END IF;
-- Update order
CHANGE $Order (
TotalAmount = $Total,
Status = 'Processed'
);
COMMIT $Order;
RETURN $Order;
END;
Activity Types
This page documents every activity type available in MDL microflows. Activities are the individual steps that make up a microflow’s logic.
Object Operations
CREATE
Creates a new object of the specified entity type and assigns attribute values:
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Status = 'Draft',
TotalAmount = 0
);
Attribute assignments are comma-separated Name = value pairs. The result is assigned to a variable.
CHANGE
Modifies attributes of an existing object:
CHANGE $Order (
Status = 'Confirmed',
TotalAmount = $CalculatedTotal
);
Multiple attributes can be changed in a single CHANGE statement.
COMMIT
Persists an object (or its changes) to the database:
COMMIT $Order;
Options:
WITH EVENTS– triggers event handlers (before/after commit microflows)REFRESH– refreshes the object in the client after committing
COMMIT $Order WITH EVENTS;
COMMIT $Order REFRESH;
COMMIT $Order WITH EVENTS REFRESH;
DELETE
Deletes an object from the database:
DELETE $Order;
ROLLBACK
Reverts uncommitted changes to an object, restoring it to its last committed state:
ROLLBACK $Order;
ROLLBACK $Order REFRESH;
The REFRESH option refreshes the object in the client.
Retrieval
RETRIEVE from Database (XPath)
Retrieves objects from the database using an optional XPath-style WHERE clause:
-- Retrieve a single object
RETRIEVE $Customer FROM Sales.Customer
WHERE Email = $InputEmail
LIMIT 1;
-- Retrieve a list of objects
RETRIEVE $ActiveOrders FROM Sales.Order
WHERE Status = 'Active';
-- Retrieve all objects of an entity
RETRIEVE $AllProducts FROM Sales.Product;
-- Retrieve with multiple conditions
RETRIEVE $RecentOrders FROM Sales.Order
WHERE Status = 'Active'
AND OrderDate > [%BeginOfCurrentDay%]
LIMIT 50;
When LIMIT 1 is specified, the result is a single entity object. Otherwise, the result is a list.
RETRIEVE by Association
Retrieves objects by following an association from an existing object:
-- Retrieve related objects via association
RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
-- Retrieve a single associated object
RETRIEVE $Customer FROM $Order/Sales.Order_Customer;
The association path uses the format $Variable/Module.AssociationName.
RETRIEVE from Variable List
Retrieves from an in-memory list variable:
RETRIEVE $Match FROM $CustomerList
WHERE Email = $SearchEmail
LIMIT 1;
Call Activities
CALL MICROFLOW
Calls another microflow, passing parameters and optionally receiving a return value:
-- Call with return value
$Total = CALL MICROFLOW Sales.SUB_CalculateTotal (
OrderLines = $Lines
);
-- Call without return value
CALL MICROFLOW Sales.SUB_SendNotification (
Customer = $Customer,
Message = 'Order confirmed'
);
-- Call with no parameters
$Config = CALL MICROFLOW Admin.GetSystemConfig ();
CALL NANOFLOW
Calls a nanoflow from within a microflow:
$Result = CALL NANOFLOW MyModule.ValidateInput (
InputValue = $Value
);
CALL JAVA ACTION
Calls a Java action:
$Hash = CALL JAVA ACTION MyModule.HashPassword (
Password = $RawPassword
);
Java action parameters follow the same Name = value syntax.
UI Activities
SHOW PAGE
Opens a page, passing parameters:
SHOW PAGE Sales.Order_Edit ($Order = $Order);
The parameter syntax uses $PageParam = $MicroflowVar. Multiple parameters are comma-separated:
SHOW PAGE Sales.OrderDetail (
$Order = $Order,
$Customer = $Customer
);
An alternate syntax using colon notation is also supported:
SHOW PAGE Sales.Order_Edit (Order: $Order);
CLOSE PAGE
Closes the current page:
CLOSE PAGE;
Validation
VALIDATION FEEDBACK
Displays a validation error message on a specific attribute of an object:
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Email address is required';
VALIDATION FEEDBACK $Order/TotalAmount MESSAGE 'Total must be greater than zero';
The syntax is VALIDATION FEEDBACK $Variable/AttributeName MESSAGE 'message text'.
Logging
LOG
Writes a message to the Mendix runtime log:
LOG INFO 'Order created successfully';
LOG WARNING 'Customer has no email address';
LOG ERROR 'Failed to process payment';
Log levels: INFO, WARNING, ERROR.
Optionally specify a log node name:
LOG INFO NODE 'OrderProcessing' 'Order created: ' + $Order/OrderNumber;
LOG ERROR NODE 'PaymentGateway' 'Payment failed for order ' + $Order/OrderNumber;
Database Query Execution
EXECUTE DATABASE QUERY
Executes a Database Connector query defined in the project:
-- Basic execution (3-part qualified name: Module.Connection.Query)
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetCustomers;
-- Dynamic SQL query
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.SearchQuery
DYNAMIC 'SELECT * FROM customers WHERE name LIKE ?';
-- With parameters
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetByEmail
PARAMETERS ($EmailParam = $Email);
-- With runtime connection override
$Result = EXECUTE DATABASE QUERY MyModule.MyConn.GetData
CONNECTION $RuntimeConnString;
The query name follows a three-part naming convention: Module.ConnectionName.QueryName.
Note: Error handling (
ON ERROR) is not supported onEXECUTE DATABASE QUERYactivities.
Summary Table
| Activity | Syntax | Returns |
|---|---|---|
| Create object | $Var = CREATE Module.Entity (Attr = val); | Entity object |
| Change object | CHANGE $Var (Attr = val); | – |
| Commit | COMMIT $Var [WITH EVENTS] [REFRESH]; | – |
| Delete | DELETE $Var; | – |
| Rollback | ROLLBACK $Var [REFRESH]; | – |
| Retrieve (DB) | RETRIEVE $Var FROM Module.Entity [WHERE ...] [LIMIT n]; | Entity or list |
| Retrieve (assoc) | RETRIEVE $Var FROM $Obj/Module.Assoc; | Entity or list |
| Call microflow | $Var = CALL MICROFLOW Module.Name (Param = $val); | Any type |
| Call nanoflow | $Var = CALL NANOFLOW Module.Name (Param = $val); | Any type |
| Call Java action | $Var = CALL JAVA ACTION Module.Name (Param = val); | Any type |
| Show page | SHOW PAGE Module.Page ($Param = $val); | – |
| Close page | CLOSE PAGE; | – |
| Validation | VALIDATION FEEDBACK $Var/Attr MESSAGE 'msg'; | – |
| Log | LOG INFO|WARNING|ERROR [NODE 'name'] 'msg'; | – |
| DB query | $Var = EXECUTE DATABASE QUERY Module.Conn.Query; | Result set |
| Assignment | SET $Var = expression; | – |
Control Flow
MDL microflows support conditional branching, loops, and error handling to control the execution path of your logic.
IF / ELSE
Conditional branching executes different activities based on a boolean expression.
Basic IF
IF $Order/TotalAmount > 1000 THEN
CHANGE $Order (DiscountApplied = true);
END IF;
IF / ELSE
IF $Customer/Email != empty THEN
CALL MICROFLOW Sales.SUB_SendEmail (Customer = $Customer);
ELSE
LOG WARNING 'Customer has no email address';
END IF;
Nested IF
Since MDL does not support CASE/WHEN (switch statements), use nested IF...ELSE blocks:
IF $Order/Status = 'Draft' THEN
CHANGE $Order (Status = 'Submitted');
ELSE
IF $Order/Status = 'Submitted' THEN
CHANGE $Order (Status = 'Approved');
ELSE
IF $Order/Status = 'Approved' THEN
CHANGE $Order (Status = 'Shipped');
ELSE
LOG WARNING 'Unexpected order status: ' + $Order/Status;
END IF;
END IF;
END IF;
Complex Conditions
Conditions support AND, OR, and parentheses:
IF $Amount > 0 AND $Customer != empty THEN
COMMIT $Order;
END IF;
IF ($Status = 'Active' OR $Status = 'Pending') AND $IsValid = true THEN
CALL MICROFLOW Sales.ProcessOrder (Order = $Order);
END IF;
LOOP (FOR EACH)
Iterates over each item in a list:
LOOP $Line IN $OrderLines
BEGIN
CHANGE $Line (
LineTotal = $Line/Quantity * $Line/UnitPrice
);
COMMIT $Line;
END LOOP;
The loop variable ($Line) is automatically declared and takes the entity type of the list.
LOOP with Nested Logic
LOOP $Order IN $PendingOrders
BEGIN
IF $Order/TotalAmount > 0 THEN
CHANGE $Order (Status = 'Confirmed');
COMMIT $Order;
ELSE
DELETE $Order;
END IF;
END LOOP;
BREAK and CONTINUE
Use BREAK to exit a loop early, and CONTINUE to skip to the next iteration:
LOOP $Item IN $Items
BEGIN
IF $Item/IsInvalid = true THEN
CONTINUE;
END IF;
IF $Item/Type = 'StopSignal' THEN
BREAK;
END IF;
CALL MICROFLOW Sales.ProcessItem (Item = $Item);
END LOOP;
WHILE Loop
Executes a block repeatedly as long as a condition remains true:
DECLARE $Counter Integer = 0;
WHILE $Counter < 10
BEGIN
SET $Counter = $Counter + 1;
LOG INFO 'Iteration: ' + toString($Counter);
END WHILE;
Caution: Ensure the condition will eventually become false to avoid infinite loops.
Error Handling
ON ERROR Suffix
Error handling is applied as a suffix to individual activities. There is no TRY...CATCH block in MDL. Instead, you specify error handling behavior on specific activities.
ON ERROR CONTINUE
Ignores the error and continues to the next activity:
COMMIT $Order ON ERROR CONTINUE;
ON ERROR ROLLBACK
Rolls back the current transaction and continues:
DELETE $Order ON ERROR ROLLBACK;
ON ERROR with Handler Block
Executes a custom error handling block when the activity fails:
COMMIT $Order ON ERROR {
LOG ERROR 'Failed to commit order: ' + $Order/OrderNumber;
ROLLBACK $Order;
};
The handler block can contain any activities – logging, rollback, showing validation messages, etc.
Error Handling Examples
-- Continue despite retrieval failure
RETRIEVE $Config FROM Admin.SystemConfig LIMIT 1 ON ERROR CONTINUE;
-- Custom error handler for external call
$Response = CALL MICROFLOW Integration.CallExternalAPI (
Payload = $RequestBody
) ON ERROR {
LOG ERROR NODE 'Integration' 'External API call failed';
SET $Response = empty;
};
-- Rollback on commit failure
COMMIT $Order WITH EVENTS ON ERROR ROLLBACK;
Note:
ON ERRORis not supported onEXECUTE DATABASE QUERYactivities.
Unsupported Control Flow
The following constructs are not supported in MDL and will cause parse errors:
| Unsupported | Use Instead |
|---|---|
CASE ... WHEN ... END CASE | Nested IF ... ELSE ... END IF |
TRY ... CATCH ... END TRY | ON ERROR { ... } blocks on individual activities |
Complete Example
CREATE MICROFLOW Sales.ACT_ProcessBatch
FOLDER 'Batch'
BEGIN
DECLARE $Orders List of Sales.Order = empty;
DECLARE $SuccessCount Integer = 0;
DECLARE $ErrorCount Integer = 0;
RETRIEVE $Orders FROM Sales.Order
WHERE Status = 'Pending';
LOOP $Order IN $Orders
BEGIN
IF $Order/TotalAmount <= 0 THEN
LOG WARNING 'Skipping order with zero amount: ' + $Order/OrderNumber;
CONTINUE;
END IF;
@caption 'Process order'
CALL MICROFLOW Sales.SUB_ProcessSingleOrder (
Order = $Order
) ON ERROR {
LOG ERROR 'Failed to process order: ' + $Order/OrderNumber;
SET $ErrorCount = $ErrorCount + 1;
CONTINUE;
};
SET $SuccessCount = $SuccessCount + 1;
END LOOP;
LOG INFO 'Batch complete: ' + toString($SuccessCount) + ' processed, '
+ toString($ErrorCount) + ' errors';
END;
Expressions
Expressions in MDL are used in conditions, attribute assignments, variable assignments, and log messages within microflows. They follow Mendix expression syntax.
Literals
String Literals
Strings are enclosed in single quotes:
'Hello, world'
'Order #1234'
To include a single quote within a string, double it:
'it''s here'
'Customer''s address'
Important: Do not use backslash escaping (
\'). Mendix expression syntax requires doubled single quotes.
Numeric Literals
42 -- Integer
3.14 -- Decimal
-100 -- Negative integer
1.5e10 -- Scientific notation
Boolean Literals
true
false
Empty
The empty literal represents a null/undefined value:
DECLARE $List List of Sales.Order = empty;
IF $Customer = empty THEN
LOG WARNING 'No customer found';
END IF;
Operators
Arithmetic Operators
| Operator | Description | Example |
|---|---|---|
+ | Addition / string concatenation | $Price + $Tax |
- | Subtraction | $Total - $Discount |
* | Multiplication | $Quantity * $UnitPrice |
div | Division | $Total div $Count |
mod | Modulo (remainder) | $Index mod 2 |
String concatenation uses +:
SET $FullName = $FirstName + ' ' + $LastName;
LOG INFO 'Processing order: ' + $Order/OrderNumber;
Comparison Operators
| Operator | Description | Example |
|---|---|---|
= | Equal | $Status = 'Active' |
!= | Not equal | $Status != 'Closed' |
> | Greater than | $Amount > 1000 |
< | Less than | $Count < 10 |
>= | Greater than or equal | $Age >= 18 |
<= | Less than or equal | $Score <= 100 |
Logical Operators
| Operator | Description | Example |
|---|---|---|
AND | Logical AND | $IsActive AND $HasEmail |
OR | Logical OR | $IsAdmin OR $IsManager |
NOT | Logical NOT | NOT $IsDeleted |
Parentheses control precedence:
IF ($Status = 'Active' OR $Status = 'Pending') AND $Amount > 0 THEN
-- ...
END IF;
Attribute Access
Access attributes of an object variable with /:
$Order/TotalAmount
$Customer/Email
$Line/Quantity
This is used in conditions, assignments, and expressions:
IF $Order/TotalAmount > 1000 THEN
SET $Discount = $Order/TotalAmount * 0.1;
END IF;
Date/Time Tokens
Mendix provides built-in date/time tokens enclosed in [% ... %]:
| Token | Description |
|---|---|
[%CurrentDateTime%] | Current date and time |
[%BeginOfCurrentDay%] | Start of today (00:00) |
[%EndOfCurrentDay%] | End of today (23:59:59) |
[%BeginOfCurrentWeek%] | Start of the current week |
[%BeginOfCurrentMonth%] | Start of the current month |
[%BeginOfCurrentYear%] | Start of the current year |
Usage in expressions:
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
DueDate = [%EndOfCurrentDay%]
);
RETRIEVE $TodayOrders FROM Sales.Order
WHERE OrderDate > [%BeginOfCurrentDay%];
String Templates
String concatenation with + is the primary way to build dynamic strings:
SET $Message = 'Order ' + $Order/OrderNumber + ' has been ' + $Order/Status;
LOG INFO NODE 'Orders' 'Total: ' + toString($Total) + ' for customer ' + $Customer/Name;
Type Conversion
Use built-in functions for type conversion in expressions:
toString($IntValue) -- Integer/Decimal/Boolean to String
Enumeration Values
Reference enumeration values with their qualified name:
$Order = CREATE Sales.Order (
Status = Sales.OrderStatus.Draft
);
IF $Order/Status = Sales.OrderStatus.Confirmed THEN
-- process confirmed order
END IF;
Alternatively, enumeration values can be referenced as plain strings when the context is unambiguous:
$Order = CREATE Sales.Order (
Status = 'Draft'
);
Expression Contexts
Expressions appear in several places within microflow activities:
In Conditions (IF, WHILE, WHERE)
IF $Order/TotalAmount > 0 AND $Order/Status != 'Cancelled' THEN ...
WHILE $Counter < $MaxRetries BEGIN ... END WHILE;
RETRIEVE $Active FROM Sales.Order WHERE Status = 'Active';
In Attribute Assignments (CREATE, CHANGE)
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Description = 'Order for ' + $Customer/Name,
TotalAmount = $Subtotal + $Tax
);
In SET Assignments
SET $Counter = $Counter + 1;
SET $IsEligible = $Customer/Age >= 18 AND $Customer/IsActive;
SET $FullName = $Customer/FirstName + ' ' + $Customer/LastName;
In LOG Messages
LOG INFO 'Processed ' + toString($Count) + ' orders';
LOG ERROR NODE 'Validation' 'Invalid amount: ' + toString($Order/TotalAmount);
In VALIDATION FEEDBACK Messages
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Please provide a valid email address';
Nanoflows vs Microflows
Nanoflows are client-side logic flows that execute in the user’s browser or on mobile devices. They share the same MDL syntax as microflows but have a different set of capabilities and restrictions.
Key Differences
| Aspect | Microflow | Nanoflow |
|---|---|---|
| Execution | Server-side (Mendix runtime) | Client-side (browser/mobile) |
| Database access | Full (retrieve, commit, delete) | No direct database access |
| Transactions | Supported (with rollback) | Not supported |
| Java actions | Supported | Not supported |
| JavaScript actions | Not supported | Supported |
| Show page | Supported | Supported |
| Close page | Supported | Supported |
| Network | Requires server round-trip | No network call (fast) |
| Offline | Not available offline | Available offline |
| Error handling | ON ERROR blocks | Limited error handling |
When to Use Which
Use a microflow when you need to:
- Retrieve data from or commit data to the database
- Call external web services or REST APIs
- Execute Java actions
- Perform batch operations on large data sets
- Use transactions with rollback support
Use a nanoflow when you need to:
- Respond quickly to user actions without server delay
- Perform client-side validation
- Toggle UI state (show/hide elements)
- Navigate between pages
- Work offline on mobile devices
CREATE NANOFLOW Syntax
CREATE [OR REPLACE] NANOFLOW <Module.Name>
[FOLDER '<path>']
BEGIN
[<declarations>]
[<activities>]
[RETURN <value>;]
END;
The syntax is identical to CREATE MICROFLOW except for the keyword.
Supported Activities in Nanoflows
Object Operations
-- Create an object (in memory only)
$Item = CREATE Sales.CartItem (
Quantity = 1,
ProductName = $Product/Name
);
-- Change an object in memory
CHANGE $Item (Quantity = $Item/Quantity + 1);
Calling Other Flows
-- Call another nanoflow
$Result = CALL NANOFLOW Sales.NAV_ValidateCart (Cart = $Cart);
-- Call a microflow (triggers server round-trip)
$ServerResult = CALL MICROFLOW Sales.ACT_SubmitOrder (Order = $Order);
UI Activities
-- Show a page
SHOW PAGE Sales.CartDetail ($Cart = $Cart);
-- Close the current page
CLOSE PAGE;
Validation
VALIDATION FEEDBACK $Item/Quantity MESSAGE 'Quantity must be at least 1';
Logging
LOG INFO 'Cart updated with ' + toString($ItemCount) + ' items';
Control Flow
IF $Cart/ItemCount = 0 THEN
VALIDATION FEEDBACK $Cart/ItemCount MESSAGE 'Cart is empty';
RETURN false;
ELSE
SHOW PAGE Sales.Checkout ($Cart = $Cart);
RETURN true;
END IF;
Activities NOT Available in Nanoflows
The following activities are server-only and cannot be used in nanoflows:
RETRIEVE ... FROM Module.Entity WHERE ...(database retrieval)COMMITDELETEROLLBACKCALL JAVA ACTIONEXECUTE DATABASE QUERYON ERROR { ... }(full error handler blocks)
SHOW and DESCRIBE
SHOW NANOFLOWS
SHOW NANOFLOWS IN MyModule
DESCRIBE NANOFLOW MyModule.NAV_ShowDetails
DROP
DROP NANOFLOW MyModule.NAV_ShowDetails;
Folder Organization
CREATE NANOFLOW Sales.NAV_OpenCart
FOLDER 'Navigation'
BEGIN
SHOW PAGE Sales.Cart_Overview ();
END;
MOVE NANOFLOW Sales.NAV_OpenCart TO FOLDER 'UI/Navigation';
Example: Client-Side Validation
CREATE NANOFLOW Sales.NAV_ValidateOrder
FOLDER 'Validation'
BEGIN
DECLARE $Order Sales.Order;
DECLARE $IsValid Boolean = true;
IF $Order/CustomerName = empty THEN
VALIDATION FEEDBACK $Order/CustomerName MESSAGE 'Customer name is required';
SET $IsValid = false;
END IF;
IF $Order/TotalAmount <= 0 THEN
VALIDATION FEEDBACK $Order/TotalAmount MESSAGE 'Total must be greater than zero';
SET $IsValid = false;
END IF;
RETURN $IsValid;
END;
Example: Page Navigation
CREATE NANOFLOW Sales.NAV_GoToOrderDetail
BEGIN
DECLARE $Order Sales.Order;
SHOW PAGE Sales.Order_Detail ($Order = $Order);
END;
Common Patterns
This page shows frequently used microflow patterns in MDL, including CRUD operations, validation, batch processing, and important anti-patterns to avoid.
CRUD Patterns
Create and Commit
CREATE MICROFLOW Sales.ACT_CreateCustomer
FOLDER 'Customer'
BEGIN
DECLARE $Name String;
DECLARE $Email String;
DECLARE $Customer Sales.Customer;
$Customer = CREATE Sales.Customer (
Name = $Name,
Email = $Email,
CreatedDate = [%CurrentDateTime%],
IsActive = true
);
COMMIT $Customer;
SHOW PAGE Sales.Customer_Edit ($Customer = $Customer);
RETURN $Customer;
END;
Retrieve and Update
CREATE MICROFLOW Sales.ACT_DeactivateCustomer
FOLDER 'Customer'
BEGIN
DECLARE $Customer Sales.Customer;
CHANGE $Customer (
IsActive = false,
DeactivatedDate = [%CurrentDateTime%]
);
COMMIT $Customer;
END;
Retrieve with Filtering
CREATE MICROFLOW Sales.ACT_GetActiveOrders
FOLDER 'Orders'
BEGIN
RETRIEVE $Orders FROM Sales.Order
WHERE Status = 'Active'
AND OrderDate > [%BeginOfCurrentMonth%];
RETURN $Orders;
END;
Delete with Confirmation
CREATE MICROFLOW Sales.ACT_DeleteOrder
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
-- Delete related order lines first
RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
LOOP $Line IN $Lines
BEGIN
DELETE $Line;
END LOOP;
DELETE $Order;
END;
Validation Pattern
Validate an object before saving, showing feedback on invalid fields:
CREATE MICROFLOW Sales.ACT_SaveCustomer
FOLDER 'Customer'
BEGIN
DECLARE $Customer Sales.Customer;
DECLARE $IsValid Boolean = true;
-- Validate required fields
IF $Customer/Name = empty THEN
VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name is required';
SET $IsValid = false;
END IF;
IF $Customer/Email = empty THEN
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Email is required';
SET $IsValid = false;
END IF;
-- Check for duplicate email
IF $IsValid THEN
RETRIEVE $Existing FROM Sales.Customer
WHERE Email = $Customer/Email
LIMIT 1;
IF $Existing != empty THEN
VALIDATION FEEDBACK $Customer/Email MESSAGE 'A customer with this email already exists';
SET $IsValid = false;
END IF;
END IF;
-- Save only if valid
IF $IsValid THEN
COMMIT $Customer;
CLOSE PAGE;
END IF;
END;
Batch Processing Pattern
Process a list of items one at a time with error tracking:
CREATE MICROFLOW Sales.ACT_ProcessPendingOrders
FOLDER 'Batch'
BEGIN
DECLARE $SuccessCount Integer = 0;
DECLARE $ErrorCount Integer = 0;
RETRIEVE $PendingOrders FROM Sales.Order
WHERE Status = 'Pending';
LOOP $Order IN $PendingOrders
BEGIN
CALL MICROFLOW Sales.SUB_ProcessOrder (
Order = $Order
) ON ERROR {
LOG ERROR NODE 'BatchProcess' 'Failed to process order: ' + $Order/OrderNumber;
SET $ErrorCount = $ErrorCount + 1;
CONTINUE;
};
SET $SuccessCount = $SuccessCount + 1;
END LOOP;
LOG INFO NODE 'BatchProcess' 'Batch complete: '
+ toString($SuccessCount) + ' succeeded, '
+ toString($ErrorCount) + ' failed';
END;
Sub-Microflow Pattern
Break complex logic into reusable sub-microflows:
-- Main microflow delegates to sub-microflow
CREATE MICROFLOW Sales.ACT_PlaceOrder
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
-- Validate
$IsValid = CALL MICROFLOW Sales.SUB_ValidateOrder (Order = $Order);
IF NOT $IsValid THEN
RETURN false;
END IF;
-- Calculate totals
CALL MICROFLOW Sales.SUB_CalculateOrderTotals (Order = $Order);
-- Finalize
CHANGE $Order (Status = 'Placed');
COMMIT $Order WITH EVENTS;
RETURN true;
END;
-- Reusable sub-microflow
CREATE MICROFLOW Sales.SUB_CalculateOrderTotals
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
DECLARE $Total Decimal = 0;
RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
LOOP $Line IN $Lines
BEGIN
SET $Total = $Total + ($Line/Quantity * $Line/UnitPrice);
END LOOP;
CHANGE $Order (TotalAmount = $Total);
END;
Association Retrieval Pattern
Retrieve related objects through associations:
CREATE MICROFLOW Sales.ACT_GetOrderSummary
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
-- Retrieve related customer
RETRIEVE $Customer FROM $Order/Sales.Order_Customer;
-- Retrieve order lines
RETRIEVE $Lines FROM $Order/Sales.OrderLine_Order;
-- Use the retrieved data
LOG INFO 'Order ' + $Order/OrderNumber
+ ' for customer ' + $Customer/Name
+ ' has ' + toString(length($Lines)) + ' lines';
RETURN $Order;
END;
Lookup Pattern (List Search)
When you have a list and need to find a matching item, retrieve from the list variable:
CREATE MICROFLOW Import.ACT_MatchDepartments
FOLDER 'Import'
BEGIN
DECLARE $Employees List of Import.Employee = empty;
DECLARE $Departments List of Import.Department = empty;
-- Retrieve all departments for lookup
RETRIEVE $Departments FROM Import.Department;
LOOP $Employee IN $Employees
BEGIN
-- O(N) lookup: retrieve from in-memory list
RETRIEVE $Dept FROM $Departments
WHERE Name = $Employee/DepartmentName
LIMIT 1;
IF $Dept != empty THEN
CHANGE $Employee (Employee_Department = $Dept);
COMMIT $Employee;
END IF;
END LOOP;
END;
Error Handling Pattern
Wrap risky operations with error handlers:
CREATE MICROFLOW Integration.ACT_SyncData
FOLDER 'Integration'
BEGIN
DECLARE $Config Integration.SyncConfig;
DECLARE $Success Boolean = false;
RETRIEVE $Config FROM Integration.SyncConfig LIMIT 1;
@caption 'Call external API'
$Response = CALL MICROFLOW Integration.SUB_CallExternalAPI (
Config = $Config
) ON ERROR {
LOG ERROR NODE 'Integration' 'External API call failed';
RETURN false;
};
IF $Response != empty THEN
@caption 'Process response'
CALL MICROFLOW Integration.SUB_ProcessResponse (
Response = $Response
) ON ERROR {
LOG ERROR NODE 'Integration' 'Failed to process response';
RETURN false;
};
SET $Success = true;
END IF;
RETURN $Success;
END;
Anti-Patterns (Avoid These)
The following patterns are common mistakes that cause bugs, performance problems, or parse errors. The mxcli check command detects these automatically.
Never Create Empty List Variables as Loop Sources
Creating an empty list and then immediately looping over it is always wrong. If you need to process a list, accept it as a microflow parameter or retrieve it from the database.
-- WRONG: empty list means the loop never executes
DECLARE $Items List of Sales.Item = empty;
LOOP $Item IN $Items
BEGIN
-- this code never runs!
END LOOP;
-- CORRECT: accept the list as a parameter
CREATE MICROFLOW Sales.ACT_ProcessItems
BEGIN
DECLARE $Items List of Sales.Item = empty; -- parameter, filled by caller
RETRIEVE $Items FROM Sales.Item WHERE Status = 'Pending'; -- or retrieve from DB
LOOP $Item IN $Items
BEGIN
-- process each item
END LOOP;
END;
Never Use Nested Loops for List Matching
Nested loops are O(N^2) and cause severe performance problems with large data sets. Instead, loop over the primary list and use RETRIEVE ... FROM $List WHERE ... LIMIT 1 to find matches.
-- WRONG: O(N^2) nested loop
LOOP $Employee IN $Employees
BEGIN
LOOP $Department IN $Departments
BEGIN
IF $Department/Name = $Employee/DeptName THEN
CHANGE $Employee (Employee_Department = $Department);
END IF;
END LOOP;
END LOOP;
-- CORRECT: O(N) lookup from list
LOOP $Employee IN $Employees
BEGIN
RETRIEVE $Dept FROM $Departments
WHERE Name = $Employee/DeptName
LIMIT 1;
IF $Dept != empty THEN
CHANGE $Employee (Employee_Department = $Dept);
END IF;
END LOOP;
Use Append Logic When Merging, Not Overwrite
When merging data from multiple sources, append new values rather than overwriting existing ones:
-- WRONG: overwrites existing notes
CHANGE $Customer (Notes = $NewNotes);
-- CORRECT: append with guard
IF $NewNotes != empty THEN
CHANGE $Customer (Notes = $Customer/Notes + '\n' + $NewNotes);
END IF;
Naming Conventions
Follow consistent naming patterns for microflows:
| Prefix | Purpose | Example |
|---|---|---|
ACT_ | User-triggered action | Sales.ACT_CreateOrder |
SUB_ | Sub-microflow (called by other microflows) | Sales.SUB_CalculateTotal |
DS_ | Data source for a page/widget | Sales.DS_GetActiveOrders |
VAL_ | Validation logic | Sales.VAL_ValidateOrder |
SE_ | Scheduled event handler | Sales.SE_ProcessBatch |
BCO_ | Before commit event | Sales.BCO_Order |
ACO_ | After commit event | Sales.ACO_Order |
Validation Checklist
Before presenting a microflow to the user, verify these rules:
- Every
DECLAREhas a valid type (String,Integer,Boolean,Decimal,DateTime,Module.Entity, orList of Module.Entity) - Entity declarations do not use
= empty(only list declarations do) - Every flow path ends with a
RETURNstatement (or the microflow returns void) - No empty list variables are used as loop sources
- No nested loops for list matching
- All entity and microflow qualified names use the
Module.Nameformat VALIDATION FEEDBACKuses the$Variable/Attribute MESSAGE 'text'syntax
Run the syntax checker to catch issues:
mxcli check script.mdl
mxcli check script.mdl -p app.mpr --references
Pages
Pages define the user interface of a Mendix application. Each page consists of a widget tree arranged within a layout, with data sources that connect widgets to the domain model.
Core Concepts
| Concept | Description |
|---|---|
| Layout | A reusable page template that defines content regions (e.g., header, sidebar, main content) |
| Widget tree | A hierarchical structure of widgets that defines the page’s visual content |
| Data source | Determines how a widget obtains its data (page parameter, database query, microflow, etc.) |
| Widget name | Every widget has a unique name within the page, used for ALTER PAGE operations |
CREATE PAGE
The basic syntax for creating a page:
CREATE [OR REPLACE] PAGE <Module>.<Name>
(
[Params: { $Param: Module.Entity [, ...] },]
Title: '<title>',
Layout: <Module.LayoutName>
[, Folder: '<path>']
)
{
<widget-tree>
}
Minimal Example
CREATE PAGE MyModule.Home
(
Title: 'Welcome',
Layout: Atlas_Core.Atlas_Default
)
{
CONTAINER cMain {
DYNAMICTEXT txtWelcome (Content: 'Welcome to the application')
}
}
Page with Parameters
Pages can receive entity objects as parameters from the calling context:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
Page Properties
| Property | Description | Example |
|---|---|---|
Params | Page parameters (entity objects passed in) | Params: { $Order: Sales.Order } |
Title | Page title shown in the browser/tab | Title: 'Edit Customer' |
Layout | Layout to use for the page | Layout: Atlas_Core.PopupLayout |
Folder | Organizational folder within the module | Folder: 'Pages/Customers' |
Variables | Page-level variables for conditional logic | Variables: { $show: Boolean = 'true' } |
Layouts
Layouts are referenced by their qualified name (Module.LayoutName). Common Atlas layouts include:
| Layout | Usage |
|---|---|
Atlas_Core.Atlas_Default | Full-page layout with navigation sidebar |
Atlas_Core.PopupLayout | Modal popup dialog |
Atlas_Core.Atlas_TopBar | Layout with top navigation bar |
DROP PAGE
Removes a page from the project:
DROP PAGE MyModule.Customer_Edit;
Inspecting Pages
Use SHOW and DESCRIBE to examine existing pages:
-- List all pages in a module
SHOW PAGES IN MyModule;
-- Show the full MDL definition of a page (round-trippable)
DESCRIBE PAGE MyModule.Customer_Edit;
The output of DESCRIBE PAGE can be used as input to CREATE OR REPLACE PAGE for round-trip editing.
See Also
- Page Structure – layout selection, content areas, and data sources
- Widget Types – full catalog of available widgets
- Data Binding – connecting widgets to entity attributes
- Snippets – reusable page fragments
- ALTER PAGE – modifying existing pages in-place
- Common Patterns – list page, edit page, master-detail patterns
Page Structure
Every MDL page has three main parts: page properties (title, layout, parameters), a layout reference that defines the page skeleton, and a widget tree that fills the content area.
CREATE PAGE Syntax
CREATE [OR REPLACE] PAGE <Module>.<Name>
(
[Params: { $Param: Module.Entity [, ...] },]
Title: '<title>',
Layout: <Module.LayoutName>
[, Folder: '<path>']
[, Variables: { $name: Type = 'expression' [, ...] }]
)
{
<widget-tree>
}
Layout Reference
The Layout property selects the page layout, which determines the overall structure (navigation sidebar, top bar, popup frame, etc.). The widget tree you define fills the layout’s content placeholder.
CREATE PAGE MyModule.CustomerList
(
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default
)
{
-- Widgets here fill the main content area of Atlas_Default
DATAGRID dgCustomers (DataSource: DATABASE MyModule.Customer) {
COLUMN colName (Attribute: Name, Caption: 'Name')
COLUMN colEmail (Attribute: Email, Caption: 'Email')
}
}
Popup layouts are typically used for edit and detail pages:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
Page Parameters
Page parameters define entity objects that must be passed when the page is opened. Parameters use the $ prefix:
(
Params: { $Customer: MyModule.Customer },
...
)
Multiple parameters are comma-separated:
(
Params: { $Order: Sales.Order, $Customer: Sales.Customer },
...
)
Inside the widget tree, a DATAVIEW binds to a parameter using DataSource: $ParamName.
Page Variables
Page variables store local state (booleans, strings, etc.) that can control widget visibility or other conditional logic:
(
Title: 'Product Detail',
Layout: Atlas_Core.Atlas_Default,
Variables: { $showDetails: Boolean = 'true' }
)
Data Sources
Data sources tell a container widget (DataView, DataGrid, ListView, Gallery) where to get its data.
Page Parameter Source
Binds to a page parameter. Used by DataView widgets for edit/detail pages:
DATAVIEW dvCustomer (DataSource: $Customer) {
-- widgets bound to Customer attributes
}
Database Source
Retrieves entities directly from the database. Optionally includes an XPath constraint:
DATAGRID dgOrders (DataSource: DATABASE Sales.Order) {
COLUMN colId (Attribute: OrderId, Caption: 'Order #')
COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
}
Microflow Source
Calls a microflow that returns a list or single object:
DATAVIEW dvDashboard (DataSource: MICROFLOW MyModule.DS_GetDashboardData) {
-- widgets bound to the returned object's attributes
}
Nanoflow Source
Same as microflow source but calls a nanoflow (runs on the client):
LISTVIEW lvRecent (DataSource: NANOFLOW MyModule.DS_GetRecentItems) {
-- widgets for each item
}
Association Source
Follows an association from a parent DataView’s object:
DATAVIEW dvCustomer (DataSource: $Customer) {
LISTVIEW lvOrders (DataSource: ASSOCIATION Customer_Order) {
-- widgets for each Order
}
}
Selection Source
Binds to the currently selected item in another list widget:
DATAGRID dgProducts (DataSource: DATABASE MyModule.Product) {
COLUMN colName (Attribute: Name)
}
DATAVIEW dvDetail (DataSource: SELECTION dgProducts) {
TEXTBOX txtDescription (Label: 'Description', Attribute: Description)
}
Widget Tree Structure
The widget tree is a nested hierarchy. Container widgets hold child widgets within { } braces. Every widget requires a unique name:
{
LAYOUTGRID grid1 {
ROW row1 {
COLUMN col1 {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
}
COLUMN col2 {
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
}
}
}
}
Folder Organization
Use the Folder property to organize pages into folders within a module:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout,
Folder: 'Customers'
)
{
...
}
Nested folders use / separators: Folder: 'Pages/Customers/Detail'. Missing folders are auto-created.
See Also
- Pages – page overview and CREATE PAGE basics
- Widget Types – full catalog of widgets
- Data Binding – attribute binding with the Attribute property
- Common Patterns – list page, edit page, master-detail examples
Widget Types
MDL supports a comprehensive set of widget types for building Mendix pages. Each widget is declared with a type keyword, a unique name, properties in parentheses, and optional child widgets in braces.
WIDGET_TYPE widgetName (Property: value, ...) [{ children }]
Widget Categories
| Category | Widgets |
|---|---|
| Layout | LAYOUTGRID, ROW, COLUMN, CONTAINER, CUSTOMCONTAINER |
| Data | DATAVIEW, LISTVIEW, DATAGRID, GALLERY |
| Input | TEXTBOX, TEXTAREA, CHECKBOX, RADIOBUTTONS, DATEPICKER, COMBOBOX |
| Display | DYNAMICTEXT, IMAGE, STATICIMAGE, DYNAMICIMAGE |
| Action | ACTIONBUTTON, LINKBUTTON |
| Navigation | NAVIGATIONLIST |
| Structure | HEADER, FOOTER, CONTROLBAR, SNIPPETCALL |
Layout Widgets
LAYOUTGRID
Creates a responsive grid with rows and columns. The primary layout mechanism for arranging widgets on a page:
LAYOUTGRID grid1 {
ROW row1 {
COLUMN col1 {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
}
COLUMN col2 {
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
}
}
ROW row2 {
COLUMN colFull {
TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
}
}
}
ROW
A row within a LAYOUTGRID. Contains one or more COLUMN children.
COLUMN
A column within a ROW. Contains any number of child widgets. Column width is determined by the layout grid system.
CONTAINER
A generic div container. Used to group widgets and apply CSS classes or styles:
CONTAINER cCard (Class: 'card mx-spacing-top-large') {
DYNAMICTEXT txtTitle (Content: 'Section Title')
TEXTBOX txtValue (Label: 'Value', Attribute: Value)
}
Properties:
| Property | Description | Example |
|---|---|---|
Class | CSS class names | Class: 'card p-3' |
Style | Inline CSS styles | Style: 'padding: 16px;' |
DesignProperties | Design property values | DesignProperties: ['Spacing top': 'Large'] |
CUSTOMCONTAINER
Similar to CONTAINER but used for custom-styled containers.
Data Widgets
DATAVIEW
Displays a single object. The central widget for detail and edit pages. Must have a data source:
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
Data source options:
DataSource: $ParamName– page parameterDataSource: MICROFLOW Module.MF_Name– microflow returning a single objectDataSource: NANOFLOW Module.NF_Name– nanoflow returning a single objectDataSource: SELECTION widgetName– currently selected item from a list widgetDataSource: ASSOCIATION AssocName– follow an association from a parent context
DATAGRID
Displays a list of objects in a tabular format with columns, sorting, and pagination:
DATAGRID dgOrders (DataSource: DATABASE Sales.Order, PageSize: 20) {
COLUMN colId (Attribute: OrderId, Caption: 'Order #')
COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
COLUMN colAmount (Attribute: Amount, Caption: 'Amount', Alignment: right)
COLUMN colStatus (Attribute: Status, Caption: 'Status')
CONTROLBAR bar1 {
ACTIONBUTTON btnNew (Caption: 'New', Action: MICROFLOW Sales.ACT_CreateOrder, ButtonStyle: Primary)
}
}
DataGrid properties:
| Property | Description | Example |
|---|---|---|
DataSource | Data source (DATABASE, MICROFLOW, etc.) | DataSource: DATABASE Module.Entity |
PageSize | Number of rows per page | PageSize: 25 |
Pagination | Pagination mode | Pagination: virtualScrolling |
PagingPosition | Position of paging controls | PagingPosition: both |
ShowPagingButtons | When to show paging buttons | ShowPagingButtons: auto |
Column properties:
| Property | Values | Default | Description |
|---|---|---|---|
Attribute | attribute name | (required) | The attribute to display |
Caption | string | attribute name | Column header text |
Alignment | left, center, right | left | Text alignment |
WrapText | true, false | false | Allow text wrapping |
Sortable | true, false | varies | Allow sorting by this column |
Resizable | true, false | true | Allow column resizing |
Draggable | true, false | true | Allow column reordering |
Hidable | yes, hidden, no | yes | User visibility toggle |
ColumnWidth | autoFill, autoFit, manual | autoFill | Width mode |
Size | integer (px) | 1 | Width in pixels (manual mode) |
Visible | expression string | true | Visibility expression |
DynamicCellClass | expression string | (empty) | Dynamic CSS class expression |
Tooltip | text string | (empty) | Column tooltip |
LISTVIEW
Displays a list of objects using a repeating template. More flexible than DataGrid for custom layouts:
LISTVIEW lvProducts (DataSource: DATABASE MyModule.Product) {
CONTAINER cItem (Class: 'list-item') {
DYNAMICTEXT txtName (Content: '{1}', Attribute: Name)
DYNAMICTEXT txtPrice (Content: '${1}', Attribute: Price)
}
}
GALLERY
A pluggable widget that displays items in a card/grid layout:
GALLERY galProducts (DataSource: DATABASE MyModule.Product) {
CONTAINER cCard {
DYNAMICTEXT txtName (Content: '{1}', Attribute: Name)
}
}
Input Widgets
All input widgets share common properties:
| Property | Description | Example |
|---|---|---|
Label | Field label text | Label: 'Customer Name' |
Attribute | Entity attribute to bind to | Attribute: Name |
Editable | Editability mode | Editable: ReadOnly |
Visible | Visibility expression | Visible: '$showField' |
TEXTBOX
Single-line text input. The most common input widget:
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
TEXTAREA
Multi-line text input for longer text:
TEXTAREA txtDescription (Label: 'Description', Attribute: Description)
CHECKBOX
Boolean (true/false) input:
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
RADIOBUTTONS
Displays enumeration or boolean values as radio buttons:
RADIOBUTTONS rbStatus (Label: 'Status', Attribute: Status)
DATEPICKER
Date and/or time input:
DATEPICKER dpBirthDate (Label: 'Birth Date', Attribute: BirthDate)
COMBOBOX
Dropdown selection for enumeration values or associations. Uses the pluggable ComboBox widget:
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
REFERENCESELECTOR
Dropdown for selecting an associated object via a reference association:
REFERENCESELECTOR rsCategory (Label: 'Category', Attribute: Category)
Display Widgets
DYNAMICTEXT
Displays dynamic text content, often with attribute values:
DYNAMICTEXT txtGreeting (Content: 'Welcome, {1}', Attribute: Name)
IMAGE / STATICIMAGE / DYNAMICIMAGE
Display images on a page:
-- Static image from the project
STATICIMAGE imgLogo (Image: 'MyModule.Logo')
-- Dynamic image from an entity attribute (entity must extend System.Image)
DYNAMICIMAGE imgPhoto (DataSource: $Photo, Width: 200, Height: 150)
-- Generic image widget
IMAGE imgBanner (Width: 800, Height: 200)
Image properties:
| Property | Description | Example |
|---|---|---|
Width | Width in pixels | Width: 200 |
Height | Height in pixels | Height: 150 |
Action Widgets
ACTIONBUTTON
A button that triggers an action. The primary interactive element:
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
Action types:
| Action | Description |
|---|---|
SAVE_CHANGES | Commit and close the page |
CANCEL_CHANGES | Roll back and close the page |
DELETE | Delete the current object |
CLOSE_PAGE | Close the page without saving |
MICROFLOW Module.MF_Name | Call a microflow |
NANOFLOW Module.NF_Name | Call a nanoflow |
PAGE Module.PageName | Open a page |
Button styles:
| Style | Typical appearance |
|---|---|
Default | Standard button |
Primary | Blue/highlighted button |
Success | Green button |
Warning | Yellow/amber button |
Danger | Red button |
Info | Light blue button |
Microflow action with parameters:
ACTIONBUTTON btnProcess (
Caption: 'Process',
Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order),
ButtonStyle: Primary
)
LINKBUTTON
Renders as a hyperlink instead of a button. Same action types as ACTIONBUTTON:
LINKBUTTON lnkDetails (Caption: 'View Details', Action: PAGE MyModule.Customer_Detail)
Structure Widgets
HEADER
Header section of a DataView, placed before the main content:
DATAVIEW dvOrder (DataSource: $Order) {
HEADER hdr1 {
DYNAMICTEXT txtOrderTitle (Content: 'Order #{1}', Attribute: OrderId)
}
TEXTBOX txtStatus (Label: 'Status', Attribute: Status)
FOOTER ftr1 { ... }
}
FOOTER
Footer section of a DataView. Typically contains save/cancel buttons:
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
CONTROLBAR
Control bar for DataGrid widgets. Contains action buttons for the grid:
CONTROLBAR bar1 {
ACTIONBUTTON btnNew (Caption: 'New', Action: MICROFLOW Module.ACT_Create, ButtonStyle: Primary)
ACTIONBUTTON btnEdit (Caption: 'Edit', Action: PAGE Module.Entity_Edit)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
}
SNIPPETCALL
Embeds a snippet (reusable page fragment) into the page:
SNIPPETCALL scNav (Snippet: MyModule.NavigationMenu)
Navigation Widgets
NAVIGATIONLIST
Renders a navigation list with clickable items:
NAVIGATIONLIST navMain {
-- navigation items
}
Common Widget Properties
These properties are shared across many widget types:
| Property | Description | Example |
|---|---|---|
Class | CSS class names | Class: 'card p-3' |
Style | Inline CSS styles | Style: 'margin-top: 8px;' |
DesignProperties | Atlas design properties | DesignProperties: ['Spacing top': 'Large', 'Full width': ON] |
Visible | Visibility expression | Visible: '$showSection' |
Editable | Editability mode | Editable: ReadOnly |
See Also
- Pages – page overview and CREATE PAGE basics
- Page Structure – layout selection and data sources
- Data Binding – connecting widgets to attributes
- ALTER PAGE – modifying widgets in existing pages
Data Binding
Data binding connects widgets to entity attributes and data sources. In MDL, binding is configured through widget properties rather than a special operator.
Attribute Binding
Input widgets bind to entity attributes using the Attribute property. The attribute name is resolved relative to the containing DataView’s entity context:
DATAVIEW dvCustomer (DataSource: $Customer) {
-- These attribute names are resolved against MyModule.Customer
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
DATEPICKER dpCreated (Label: 'Created', Attribute: CreatedDate)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
}
The SDK automatically resolves short attribute names to fully qualified paths. For example, Name is resolved to MyModule.Customer.Name based on the DataView’s entity context.
Data Sources
Data sources determine where a container widget gets its data. Different widget types support different data source types.
Page Parameter Source
The simplest binding – connects a DataView to a page parameter:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
}
}
Database Source
Retrieves entities directly from the database. Works with list widgets (DataGrid, ListView, Gallery):
DATAGRID dgCustomers (DataSource: DATABASE MyModule.Customer) {
COLUMN colName (Attribute: Name, Caption: 'Customer Name')
COLUMN colEmail (Attribute: Email, Caption: 'Email')
}
Microflow Source
Calls a microflow to retrieve data. The microflow must return the appropriate type (single object for DataView, list for DataGrid/ListView):
-- Single object for a DataView
DATAVIEW dvStats (DataSource: MICROFLOW MyModule.DS_GetStatistics) {
DYNAMICTEXT txtCount (Content: 'Total: {1}', Attribute: TotalCount)
}
-- List for a DataGrid
DATAGRID dgFiltered (DataSource: MICROFLOW MyModule.DS_GetFilteredOrders) {
COLUMN colId (Attribute: OrderId)
}
Nanoflow Source
Same as microflow source but runs on the client side:
LISTVIEW lvRecent (DataSource: NANOFLOW MyModule.DS_GetRecentItems) {
DYNAMICTEXT txtItem (Content: '{1}', Attribute: Name)
}
Association Source
Follows an association from a parent DataView to retrieve related objects:
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
-- Follow Customer_Order association to list orders
LISTVIEW lvOrders (DataSource: ASSOCIATION Customer_Order) {
DYNAMICTEXT txtOrderDate (Content: '{1}', Attribute: OrderDate)
DYNAMICTEXT txtAmount (Content: '${1}', Attribute: Amount)
}
}
Selection Source
Binds to the currently selected item in another list widget. Enables master-detail patterns:
-- Master: list of products
DATAGRID dgProducts (DataSource: DATABASE MyModule.Product) {
COLUMN colName (Attribute: Name)
COLUMN colPrice (Attribute: Price)
}
-- Detail: shows the selected product
DATAVIEW dvDetail (DataSource: SELECTION dgProducts) {
TEXTBOX txtDescription (Label: 'Description', Attribute: Description)
TEXTBOX txtCategory (Label: 'Category', Attribute: Category)
}
Nested Data Contexts
Widgets inherit the data context from their parent container. A DataView inside a DataView creates a nested context:
DATAVIEW dvOrder (DataSource: $Order) {
TEXTBOX txtOrderId (Label: 'Order #', Attribute: OrderId)
-- Nested DataView for the order's customer (via association)
DATAVIEW dvCustomer (DataSource: ASSOCIATION Order_Customer) {
-- Attributes here resolve against Customer
DYNAMICTEXT txtCustomerName (Content: '{1}', Attribute: Name)
}
}
Dynamic Text Content
The DYNAMICTEXT widget uses {1}, {2}, etc. as placeholders for attribute values in the Content property. The Attribute property specifies which attribute fills the first placeholder:
DYNAMICTEXT txtPrice (Content: 'Price: ${1}', Attribute: Price)
Button Actions with Parameters
Action buttons can pass the current data context to microflows and pages:
DATAVIEW dvOrder (DataSource: $Order) {
ACTIONBUTTON btnProcess (
Caption: 'Process Order',
Action: MICROFLOW Sales.ACT_ProcessOrder(Order: $Order),
ButtonStyle: Primary
)
ACTIONBUTTON btnEdit (
Caption: 'Edit',
Action: PAGE Sales.Order_Edit
)
}
See Also
- Pages – page overview
- Page Structure – data source types in detail
- Widget Types – available widgets and their properties
- Common Patterns – master-detail and other binding patterns
Snippets
Snippets are reusable page fragments that can be embedded in multiple pages. They allow you to define a widget tree once and include it wherever needed, promoting consistency and reducing duplication.
CREATE SNIPPET
CREATE [OR REPLACE] SNIPPET <Module>.<Name>
(
[Params: { $Param: Module.Entity [, ...] },]
[Folder: '<path>']
)
{
<widget-tree>
}
Basic Snippet
A snippet without parameters:
CREATE SNIPPET MyModule.Footer
(
Folder: 'Snippets'
)
{
CONTAINER cFooter (Class: 'app-footer') {
DYNAMICTEXT txtCopyright (Content: '2024 My Company. All rights reserved.')
}
}
Snippet with Parameters
Snippets can accept entity parameters, similar to pages:
CREATE SNIPPET MyModule.CustomerCard
(
Params: { $Customer: MyModule.Customer }
)
{
CONTAINER cCard (Class: 'card') {
DATAVIEW dvCustomer (DataSource: $Customer) {
DYNAMICTEXT txtName (Content: '{1}', Attribute: Name)
DYNAMICTEXT txtEmail (Content: '{1}', Attribute: Email)
ACTIONBUTTON btnEdit (
Caption: 'Edit',
Action: PAGE MyModule.Customer_Edit,
ButtonStyle: Primary
)
}
}
}
Using Snippets in Pages
Embed a snippet in a page using the SNIPPETCALL widget:
CREATE PAGE MyModule.Home
(
Title: 'Home',
Layout: Atlas_Core.Atlas_Default
)
{
CONTAINER cMain {
SNIPPETCALL scFooter (Snippet: MyModule.Footer)
}
}
Inspecting Snippets
-- List all snippets in a module
SHOW SNIPPETS IN MyModule;
-- View full MDL definition
DESCRIBE SNIPPET MyModule.CustomerCard;
DROP SNIPPET
DROP SNIPPET MyModule.Footer;
ALTER SNIPPET
Snippets support the same in-place modification operations as pages. See ALTER PAGE / ALTER SNIPPET:
ALTER SNIPPET MyModule.CustomerCard {
SET Caption = 'View Details' ON btnEdit;
INSERT AFTER txtEmail {
DYNAMICTEXT txtPhone (Content: '{1}', Attribute: Phone)
}
};
See Also
- Pages – page overview
- Widget Types – available widgets including SNIPPETCALL
- ALTER PAGE – modifying snippets and pages in-place
- Common Patterns – patterns that use snippets
ALTER PAGE / ALTER SNIPPET
The ALTER PAGE and ALTER SNIPPET statements modify an existing page or snippet’s widget tree in-place, without requiring a full CREATE OR REPLACE. This is especially useful for incremental changes: adding a field, changing a button caption, or removing an unused widget.
ALTER operates directly on the raw widget tree, preserving any widget types that MDL does not natively support (pluggable widgets, custom widgets, etc.).
Syntax
ALTER PAGE <Module>.<Name> {
<operations>
};
ALTER SNIPPET <Module>.<Name> {
<operations>
};
Operations
SET – Modify Widget Properties
Change one or more properties on a widget identified by name:
-- Single property
ALTER PAGE Module.EditPage {
SET Caption = 'Save & Close' ON btnSave
};
-- Multiple properties at once
ALTER PAGE Module.EditPage {
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave
};
Supported SET properties:
| Property | Description | Example |
|---|---|---|
Caption | Button/link caption | SET Caption = 'Submit' ON btnSave |
Label | Input field label | SET Label = 'Full Name' ON txtName |
ButtonStyle | Button visual style | SET ButtonStyle = Danger ON btnDelete |
Class | CSS class names | SET Class = 'card p-3' ON cMain |
Style | Inline CSS | SET Style = 'margin: 8px;' ON cBox |
Editable | Editability mode | SET Editable = ReadOnly ON txtEmail |
Visible | Visibility expression | SET Visible = '$showField' ON txtPhone |
Name | Widget name | SET Name = 'txtFullName' ON txtName |
SET – Page-Level Properties
Omit the ON clause to set page-level properties:
ALTER PAGE Module.EditPage {
SET Title = 'Customer Details'
};
SET – Pluggable Widget Properties
Use quoted property names to set properties on pluggable widgets (ComboBox, DataGrid2, etc.):
ALTER PAGE Module.EditPage {
SET 'showLabel' = false ON cbStatus
};
INSERT – Add Widgets
Insert new widgets before or after an existing widget:
-- Insert after a widget
ALTER PAGE Module.EditPage {
INSERT AFTER txtEmail {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
TEXTBOX txtFax (Label: 'Fax', Attribute: Fax)
}
};
-- Insert before a widget
ALTER PAGE Module.EditPage {
INSERT BEFORE btnSave {
ACTIONBUTTON btnPreview (Caption: 'Preview', Action: MICROFLOW Module.ACT_Preview)
}
};
The inserted widgets use the same syntax as in CREATE PAGE. Multiple widgets can be inserted in a single block.
DROP WIDGET – Remove Widgets
Remove one or more widgets by name:
ALTER PAGE Module.EditPage {
DROP WIDGET txtUnused
};
-- Multiple widgets
ALTER PAGE Module.EditPage {
DROP WIDGET txtFax, txtPager, btnObsolete
};
Dropping a container widget also removes all of its children.
REPLACE – Replace a Widget
Replace a widget (and its subtree) with new widgets:
ALTER PAGE Module.EditPage {
REPLACE txtOldField WITH {
TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
}
};
Page Variables
Add or remove page-level variables:
-- Add a variable
ALTER PAGE Module.EditPage {
ADD Variables $showAdvanced: Boolean = 'false'
};
-- Remove a variable
ALTER PAGE Module.EditPage {
DROP Variables $showAdvanced
};
Combining Operations
Multiple operations can be combined in a single ALTER statement. They are applied in order:
ALTER PAGE Module.Customer_Edit {
-- Change button appearance
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;
-- Remove unused fields
DROP WIDGET txtFax;
-- Add new fields after email
INSERT AFTER txtEmail {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
TEXTBOX txtMobile (Label: 'Mobile', Attribute: Mobile)
};
-- Replace old status dropdown with combobox
REPLACE ddStatus WITH {
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
}
};
Workflow Tips
-
Discover widget names first – Run
DESCRIBE PAGE Module.PageNameto see the current widget tree with all widget names. -
Use ALTER for small changes – For adding a field or changing a caption, ALTER is faster and safer than
CREATE OR REPLACE, because it preserves widgets that MDL cannot round-trip (pluggable widgets with complex configurations). -
Use CREATE OR REPLACE for major rewrites – When restructuring the entire page layout, a full replacement is cleaner.
Examples
Add a Field to an Edit Page
-- First, check what's on the page
DESCRIBE PAGE MyModule.Customer_Edit;
-- Add a phone field after email
ALTER PAGE MyModule.Customer_Edit {
INSERT AFTER txtEmail {
TEXTBOX txtPhone (Label: 'Phone Number', Attribute: Phone)
}
};
Change Button Behavior
ALTER PAGE MyModule.Order_Edit {
SET (Caption = 'Submit Order', ButtonStyle = Success) ON btnSave;
SET Caption = 'Discard' ON btnCancel
};
Add a DataGrid Column
Since DataGrid columns are part of the widget tree, use INSERT to add columns:
ALTER PAGE MyModule.Customer_Overview {
INSERT AFTER colEmail {
COLUMN colPhone (Attribute: Phone, Caption: 'Phone')
}
};
See Also
- Pages – page overview and CREATE PAGE basics
- Widget Types – widgets available for INSERT and REPLACE
- Snippets – ALTER SNIPPET uses the same operations
- Common Patterns – page layout patterns
Common Patterns
This page collects frequently used page patterns: overview/list pages, edit pages, and master-detail layouts. Each pattern is a complete, working example.
Overview / List Page
An overview page displays a list of entities with a control bar for creating, editing, and deleting records. Clicking a row opens the edit page.
CREATE PAGE MyModule.Customer_Overview
(
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Customers'
)
{
DATAGRID dgCustomers (DataSource: DATABASE MyModule.Customer, PageSize: 20) {
COLUMN colName (Attribute: Name, Caption: 'Name')
COLUMN colEmail (Attribute: Email, Caption: 'Email')
COLUMN colStatus (Attribute: Status, Caption: 'Status')
COLUMN colCreated (Attribute: CreatedDate, Caption: 'Created')
CONTROLBAR bar1 {
ACTIONBUTTON btnNew (
Caption: 'New Customer',
Action: MICROFLOW MyModule.ACT_Customer_New,
ButtonStyle: Primary
)
ACTIONBUTTON btnEdit (Caption: 'Edit', Action: PAGE MyModule.Customer_Edit)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
}
}
}
The “New Customer” button calls a microflow that creates a new Customer object and opens the edit page:
CREATE MICROFLOW MyModule.ACT_Customer_New
BEGIN
DECLARE $Customer MyModule.Customer;
$Customer = CREATE MyModule.Customer (
IsActive = true
);
SHOW PAGE MyModule.Customer_Edit ($Customer = $Customer);
RETURN $Customer;
END;
Edit Page (Popup)
An edit page displayed as a popup dialog. Receives the entity as a page parameter:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout,
Folder: 'Customers'
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
Detail Page (Full Page)
A full-page detail view with sections organized using layout grids:
CREATE PAGE MyModule.Customer_Detail
(
Params: { $Customer: MyModule.Customer },
Title: 'Customer Detail',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Customers'
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
LAYOUTGRID grid1 {
ROW rowBasic {
COLUMN col1 {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
}
COLUMN col2 {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
}
}
ROW rowNotes {
COLUMN colNotes {
TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
}
}
}
FOOTER footer1 {
ACTIONBUTTON btnEdit (
Caption: 'Edit',
Action: PAGE MyModule.Customer_Edit,
ButtonStyle: Primary
)
ACTIONBUTTON btnBack (Caption: 'Back', Action: CLOSE_PAGE)
}
}
}
Master-Detail Pattern
A master-detail page shows a list on one side and the selected item’s details on the other. The SELECTION data source connects the detail view to the list.
Side-by-Side Layout
CREATE PAGE MyModule.Product_MasterDetail
(
Title: 'Products',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Products'
)
{
LAYOUTGRID gridMain {
ROW rowContent {
COLUMN colList {
DATAGRID dgProducts (DataSource: DATABASE MyModule.Product, PageSize: 15) {
COLUMN colName (Attribute: Name, Caption: 'Product')
COLUMN colPrice (Attribute: Price, Caption: 'Price', Alignment: right)
COLUMN colCategory (Attribute: Category, Caption: 'Category')
CONTROLBAR bar1 {
ACTIONBUTTON btnNew (
Caption: 'New',
Action: MICROFLOW MyModule.ACT_Product_New,
ButtonStyle: Primary
)
}
}
}
COLUMN colDetail {
DATAVIEW dvDetail (DataSource: SELECTION dgProducts) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtDescription (Label: 'Description', Attribute: Description)
TEXTBOX txtPrice (Label: 'Price', Attribute: Price)
COMBOBOX cbCategory (Label: 'Category', Attribute: Category)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
}
}
}
}
}
}
With Related Items
A master-detail pattern where selecting an entity also shows its related child entities via an association:
CREATE PAGE MyModule.Order_MasterDetail
(
Title: 'Orders',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Orders'
)
{
LAYOUTGRID gridMain {
ROW rowContent {
COLUMN colOrders {
DATAGRID dgOrders (DataSource: DATABASE MyModule.Order, PageSize: 10) {
COLUMN colOrderId (Attribute: OrderId, Caption: 'Order #')
COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
COLUMN colStatus (Attribute: Status, Caption: 'Status')
COLUMN colTotal (Attribute: TotalAmount, Caption: 'Total', Alignment: right)
}
}
COLUMN colDetail {
DATAVIEW dvOrder (DataSource: SELECTION dgOrders) {
DYNAMICTEXT txtOrderInfo (Content: 'Order #{1}', Attribute: OrderId)
TEXTBOX txtStatus (Label: 'Status', Attribute: Status)
-- Order lines via association
DATAGRID dgLines (DataSource: ASSOCIATION Order_OrderLine) {
COLUMN colProduct (Attribute: ProductName, Caption: 'Product')
COLUMN colQty (Attribute: Quantity, Caption: 'Qty', Alignment: right)
COLUMN colLineTotal (Attribute: LineTotal, Caption: 'Total', Alignment: right)
}
FOOTER footer1 {
ACTIONBUTTON btnEdit (
Caption: 'Edit Order',
Action: PAGE MyModule.Order_Edit,
ButtonStyle: Primary
)
}
}
}
}
}
}
CRUD Page Set
A complete set of pages for managing an entity typically includes:
- Overview page – DataGrid with list of records + New/Edit/Delete buttons
- Edit page (popup) – DataView with input fields + Save/Cancel
- New microflow – Creates a blank object and opens the edit page
Here is a concise CRUD set for an Employee entity:
-- 1. Overview page
CREATE PAGE HR.Employee_Overview
(
Title: 'Employees',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Employees'
)
{
DATAGRID dgEmployees (DataSource: DATABASE HR.Employee, PageSize: 20) {
COLUMN colName (Attribute: FullName, Caption: 'Name')
COLUMN colDept (Attribute: Department, Caption: 'Department')
COLUMN colHireDate (Attribute: HireDate, Caption: 'Hire Date')
CONTROLBAR bar1 {
ACTIONBUTTON btnNew (
Caption: 'New Employee',
Action: MICROFLOW HR.ACT_Employee_New,
ButtonStyle: Primary
)
ACTIONBUTTON btnEdit (Caption: 'Edit', Action: PAGE HR.Employee_Edit)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
}
}
}
-- 2. Edit page (popup)
CREATE PAGE HR.Employee_Edit
(
Params: { $Employee: HR.Employee },
Title: 'Edit Employee',
Layout: Atlas_Core.PopupLayout,
Folder: 'Employees'
)
{
DATAVIEW dvEmployee (DataSource: $Employee) {
TEXTBOX txtName (Label: 'Full Name', Attribute: FullName)
TEXTBOX txtDept (Label: 'Department', Attribute: Department)
DATEPICKER dpHireDate (Label: 'Hire Date', Attribute: HireDate)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
-- 3. New employee microflow
CREATE MICROFLOW HR.ACT_Employee_New
BEGIN
DECLARE $Employee HR.Employee;
$Employee = CREATE HR.Employee (
HireDate = [%CurrentDateTime%]
);
SHOW PAGE HR.Employee_Edit ($Employee = $Employee);
RETURN $Employee;
END;
Dashboard Page
A dashboard page using layout grids to arrange multiple data sections:
CREATE PAGE MyModule.Dashboard
(
Title: 'Dashboard',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Dashboard'
)
{
LAYOUTGRID gridDash {
ROW rowTop {
COLUMN colRecent {
CONTAINER cRecent (Class: 'card') {
DYNAMICTEXT txtRecentTitle (Content: 'Recent Orders')
DATAGRID dgRecent (DataSource: MICROFLOW MyModule.DS_RecentOrders, PageSize: 5) {
COLUMN colOrderId (Attribute: OrderId, Caption: 'Order #')
COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
COLUMN colStatus (Attribute: Status, Caption: 'Status')
}
}
}
COLUMN colStats {
CONTAINER cStats (Class: 'card') {
DATAVIEW dvStats (DataSource: MICROFLOW MyModule.DS_GetStatistics) {
DYNAMICTEXT txtTotal (Content: 'Total Orders: {1}', Attribute: TotalOrders)
DYNAMICTEXT txtRevenue (Content: 'Revenue: ${1}', Attribute: TotalRevenue)
}
}
}
}
}
}
See Also
- Pages – page overview
- Page Structure – data sources and layout references
- Widget Types – available widgets
- ALTER PAGE – incremental modifications to existing pages
- Snippets – reusable page fragments
Security
Mendix applications use a layered security model with three levels: project security settings, role definitions, and access rules. MDL provides complete control over all three layers.
Security Levels
The project security level determines how strictly the runtime enforces access rules.
| Level | MDL Keyword | Description |
|---|---|---|
| Off | OFF | No security enforcement (development only) |
| Prototype | PROTOTYPE | Security enforced but incomplete configurations allowed |
| Production | PRODUCTION | Full enforcement, all access rules must be complete |
ALTER PROJECT SECURITY LEVEL PRODUCTION;
Security Architecture
Security in Mendix is organized in layers:
- Module Roles – defined per module, they represent permissions within that module (e.g.,
Shop.Admin,Shop.Viewer) - User Roles – project-level roles that aggregate module roles across modules (e.g.,
AppAdmincombinesShop.AdminandSystem.Administrator) - Entity Access – CRUD permissions and XPath constraints per entity per module role
- Document Access – execute/view permissions on microflows, nanoflows, and pages per module role
- Demo Users – test accounts with assigned user roles for development
Inspecting Security
MDL provides several commands for viewing the current security configuration:
-- Project-wide settings
SHOW PROJECT SECURITY;
-- Roles
SHOW MODULE ROLES;
SHOW MODULE ROLES IN Shop;
SHOW USER ROLES;
-- Access rules
SHOW ACCESS ON MICROFLOW Shop.ACT_ProcessOrder;
SHOW ACCESS ON PAGE Shop.Order_Edit;
SHOW ACCESS ON Shop.Customer;
-- Full matrix
SHOW SECURITY MATRIX;
SHOW SECURITY MATRIX IN Shop;
-- Demo users
SHOW DEMO USERS;
Modifying Project Security
Toggle the security level and demo user visibility:
ALTER PROJECT SECURITY LEVEL PRODUCTION;
ALTER PROJECT SECURITY DEMO USERS ON;
ALTER PROJECT SECURITY DEMO USERS OFF;
See Also
- Module Roles and User Roles – defining roles
- Entity Access – CRUD permissions per entity
- Document Access – microflow, page, and nanoflow permissions
- GRANT / REVOKE – granting and revoking permissions
- Demo Users – creating test accounts
Module Roles and User Roles
Mendix security uses a two-tier role system. Module roles define permissions within a single module. User roles aggregate module roles across the entire project.
Module Roles
A module role represents a set of permissions within a module. Entity access rules, microflow access, and page access are all granted to module roles.
CREATE MODULE ROLE
CREATE MODULE ROLE <Module>.<Role> [DESCRIPTION '<text>'];
Examples:
CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full administrative access';
CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard customer-facing role';
CREATE MODULE ROLE Shop.Viewer;
DROP MODULE ROLE
DROP MODULE ROLE Shop.Viewer;
Listing Module Roles
SHOW MODULE ROLES;
SHOW MODULE ROLES IN Shop;
User Roles
A user role is a project-level role assigned to end users at login. Each user role includes one or more module roles, granting the user the combined permissions of all included module roles.
CREATE USER ROLE
CREATE USER ROLE <Name> (<Module>.<Role> [, ...]) [MANAGE ALL ROLES];
The MANAGE ALL ROLES option allows users with this role to assign any user role to other users (typically for administrators).
Examples:
-- Administrator with management rights
CREATE USER ROLE AppAdmin (Shop.Admin, System.Administrator) MANAGE ALL ROLES;
-- Regular user
CREATE USER ROLE AppUser (Shop.User);
-- Read-only viewer
CREATE USER ROLE AppViewer (Shop.Viewer);
ALTER USER ROLE
Add or remove module roles from an existing user role:
ALTER USER ROLE AppAdmin ADD MODULE ROLES (Reporting.Admin);
ALTER USER ROLE AppUser REMOVE MODULE ROLES (Shop.Viewer);
DROP USER ROLE
DROP USER ROLE AppViewer;
Listing User Roles
SHOW USER ROLES;
Typical Setup
A common pattern is to create module roles first, then compose them into user roles:
-- 1. Module roles
CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full shop access';
CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard shop access';
CREATE MODULE ROLE Reporting.Viewer DESCRIPTION 'View reports';
-- 2. User roles
CREATE USER ROLE Administrator (Shop.Admin, Reporting.Viewer, System.Administrator) MANAGE ALL ROLES;
CREATE USER ROLE Employee (Shop.User, Reporting.Viewer);
CREATE USER ROLE Guest (Shop.User);
See Also
- Security – overview of the security model
- Entity Access – granting CRUD permissions to module roles
- Document Access – granting microflow and page access
- GRANT / REVOKE – the GRANT and REVOKE statements
- Demo Users – creating test accounts with user roles
Entity Access
Entity access rules control which module roles can create, read, write, and delete objects of a given entity. Rules can restrict access to specific attributes and apply XPath constraints to limit which rows are visible.
GRANT on Entities
GRANT <Module>.<Role> ON <Module>.<Entity> (<rights>) [WHERE '<xpath>'];
The <rights> list is a comma-separated combination of:
| Right | Syntax | Description |
|---|---|---|
| Create | CREATE | Allow creating new objects |
| Delete | DELETE | Allow deleting objects |
| Read all | READ * | Read access to all attributes and associations |
| Read specific | READ (<attr>, ...) | Read access to listed members only |
| Write all | WRITE * | Write access to all attributes and associations |
| Write specific | WRITE (<attr>, ...) | Write access to listed members only |
Examples
Full Access
Grant all operations on all members:
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);
Read-Only Access
GRANT Shop.Viewer ON Shop.Customer (READ *);
Selective Member Access
Restrict read and write to specific attributes:
GRANT Shop.User ON Shop.Customer (READ (Name, Email, Status), WRITE (Email));
XPath Constraints
Limit which objects a role can see or modify using an XPath expression in the WHERE clause:
-- Users can only access their own orders
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
WHERE '[Sales.Order_Customer/Sales.Customer/Name = $currentUser]';
-- Only open orders are editable
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
WHERE '[Status = ''Open'']';
Note that single quotes inside XPath expressions must be doubled (''), since the entire expression is wrapped in single quotes.
Multiple Roles on the Same Entity
Each GRANT creates a separate access rule. An entity can have rules for multiple roles:
GRANT Shop.Admin ON Shop.Order (CREATE, DELETE, READ *, WRITE *);
GRANT Shop.User ON Shop.Order (READ *, WRITE *) WHERE '[Status = ''Open'']';
GRANT Shop.Viewer ON Shop.Order (READ *);
REVOKE on Entities
Remove an entity access rule for a role:
REVOKE <Module>.<Role> ON <Module>.<Entity>;
Example:
REVOKE Shop.Viewer ON Shop.Customer;
This removes the entire access rule for that role on that entity.
Viewing Entity Access
-- See which roles have access to an entity
SHOW ACCESS ON Shop.Customer;
-- Full matrix across a module
SHOW SECURITY MATRIX IN Shop;
See Also
- Security – overview of the security model
- Module Roles and User Roles – defining the roles referenced in access rules
- Document Access – microflow and page access
- GRANT / REVOKE – complete GRANT and REVOKE reference
Microflow, Page, and Nanoflow Access
Document-level access controls which module roles can execute microflows and nanoflows or view pages. Without an explicit grant, a document is inaccessible to all roles.
Microflow Access
GRANT EXECUTE ON MICROFLOW
GRANT EXECUTE ON MICROFLOW <Module>.<Name> TO <Module>.<Role> [, ...];
Examples:
-- Single role
GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.Admin;
-- Multiple roles
GRANT EXECUTE ON MICROFLOW Shop.ACT_ViewOrders TO Shop.User, Shop.Admin;
REVOKE EXECUTE ON MICROFLOW
REVOKE EXECUTE ON MICROFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];
Example:
REVOKE EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder FROM Shop.User;
Page Access
GRANT VIEW ON PAGE
GRANT VIEW ON PAGE <Module>.<Name> TO <Module>.<Role> [, ...];
Examples:
GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;
GRANT VIEW ON PAGE Shop.Admin_Dashboard TO Shop.Admin;
REVOKE VIEW ON PAGE
REVOKE VIEW ON PAGE <Module>.<Name> FROM <Module>.<Role> [, ...];
Example:
REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;
Nanoflow Access
Nanoflow access uses the same syntax as microflow access:
GRANT EXECUTE ON NANOFLOW Shop.NAV_Filter TO Shop.User, Shop.Admin;
REVOKE EXECUTE ON NANOFLOW Shop.NAV_Filter FROM Shop.User;
Viewing Document Access
SHOW ACCESS ON MICROFLOW Shop.ACT_ProcessOrder;
SHOW ACCESS ON PAGE Shop.Order_Overview;
Typical Pattern
After creating documents, grant access as part of the same script:
-- Create the microflow
CREATE MICROFLOW Shop.ACT_CreateOrder
BEGIN
DECLARE $Order Shop.Order;
$Order = CREATE Shop.Order (Status = 'Draft');
COMMIT $Order;
RETURN $Order;
END;
-- Grant access
GRANT EXECUTE ON MICROFLOW Shop.ACT_CreateOrder TO Shop.User, Shop.Admin;
-- Create the page
CREATE PAGE Shop.Order_Edit (
Params: { $Order: Shop.Order },
Title: 'Edit Order',
Layout: Atlas_Core.PopupLayout
) { ... }
-- Grant access
GRANT VIEW ON PAGE Shop.Order_Edit TO Shop.User, Shop.Admin;
See Also
- Security – overview of the security model
- Entity Access – CRUD permissions on entities
- GRANT / REVOKE – complete GRANT and REVOKE reference
- Module Roles and User Roles – defining the roles
GRANT / REVOKE
The GRANT and REVOKE statements control all permissions in a Mendix project. They work on three targets: entities (CRUD access), microflows (execute access), and pages (view access).
Entity Access
GRANT
GRANT <Module>.<Role> ON <Module>.<Entity> (<rights>) [WHERE '<xpath>'];
Where <rights> is a comma-separated list of:
| Right | Description |
|---|---|
CREATE | Allow creating new objects |
DELETE | Allow deleting objects |
READ * | Read all members |
READ (<attr>, ...) | Read specific members only |
WRITE * | Write all members |
WRITE (<attr>, ...) | Write specific members only |
Examples:
-- Full access
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);
-- Read-only
GRANT Shop.Viewer ON Shop.Customer (READ *);
-- Selective members
GRANT Shop.User ON Shop.Customer (READ (Name, Email), WRITE (Email));
-- With XPath constraint (doubled single quotes for string literals)
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
WHERE '[Status = ''Open'']';
REVOKE
Remove an entity access rule entirely:
REVOKE <Module>.<Role> ON <Module>.<Entity>;
Example:
REVOKE Shop.Viewer ON Shop.Customer;
Microflow Access
GRANT EXECUTE ON MICROFLOW
GRANT EXECUTE ON MICROFLOW <Module>.<Name> TO <Module>.<Role> [, ...];
Example:
GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.User, Shop.Admin;
REVOKE EXECUTE ON MICROFLOW
REVOKE EXECUTE ON MICROFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];
Example:
REVOKE EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder FROM Shop.User;
Page Access
GRANT VIEW ON PAGE
GRANT VIEW ON PAGE <Module>.<Name> TO <Module>.<Role> [, ...];
Example:
GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;
REVOKE VIEW ON PAGE
REVOKE VIEW ON PAGE <Module>.<Name> FROM <Module>.<Role> [, ...];
Example:
REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;
Nanoflow Access
GRANT EXECUTE ON NANOFLOW <Module>.<Name> TO <Module>.<Role> [, ...];
REVOKE EXECUTE ON NANOFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];
Workflow Access
GRANT EXECUTE ON WORKFLOW <Module>.<Name> TO <Module>.<Role> [, ...];
REVOKE EXECUTE ON WORKFLOW <Module>.<Name> FROM <Module>.<Role> [, ...];
OData Service Access
GRANT ACCESS ON ODATA SERVICE <Module>.<Name> TO <Module>.<Role> [, ...];
REVOKE ACCESS ON ODATA SERVICE <Module>.<Name> FROM <Module>.<Role> [, ...];
Complete Example
A typical security setup script:
-- Module roles
CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full access';
CREATE MODULE ROLE Shop.User DESCRIPTION 'Standard access';
CREATE MODULE ROLE Shop.Viewer DESCRIPTION 'Read-only access';
-- User roles
CREATE USER ROLE Administrator (Shop.Admin, System.Administrator) MANAGE ALL ROLES;
CREATE USER ROLE Employee (Shop.User);
CREATE USER ROLE Guest (Shop.Viewer);
-- Entity access
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);
GRANT Shop.User ON Shop.Customer (READ *, WRITE (Email, Phone));
GRANT Shop.Viewer ON Shop.Customer (READ *);
GRANT Shop.Admin ON Shop.Order (CREATE, DELETE, READ *, WRITE *);
GRANT Shop.User ON Shop.Order (CREATE, READ *, WRITE *)
WHERE '[Status = ''Open'']';
GRANT Shop.Viewer ON Shop.Order (READ *);
-- Microflow access
GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.Admin;
GRANT EXECUTE ON MICROFLOW Shop.ACT_CreateOrder TO Shop.User, Shop.Admin;
GRANT EXECUTE ON MICROFLOW Shop.ACT_ViewOrders TO Shop.User, Shop.Admin, Shop.Viewer;
-- Page access
GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin, Shop.Viewer;
GRANT VIEW ON PAGE Shop.Order_Edit TO Shop.User, Shop.Admin;
GRANT VIEW ON PAGE Shop.Admin_Dashboard TO Shop.Admin;
-- Demo users
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!' (Administrator);
CREATE DEMO USER 'demo_user' PASSWORD 'User123!' (Employee);
-- Enable demo users
ALTER PROJECT SECURITY DEMO USERS ON;
ALTER PROJECT SECURITY LEVEL PROTOTYPE;
See Also
- Security – overview of the security model
- Entity Access – details on entity CRUD permissions and XPath constraints
- Document Access – microflow, page, and nanoflow access patterns
- Module Roles and User Roles – creating and managing roles
- Demo Users – creating test accounts
Demo Users
Demo users are test accounts created for development and testing. They appear on the login screen when demo users are enabled, allowing quick access without manual user setup.
CREATE DEMO USER
CREATE DEMO USER '<username>' PASSWORD '<password>'
[ENTITY <Module>.<Entity>]
(<UserRole> [, ...]);
| Parameter | Description |
|---|---|
<username> | Login name for the demo user |
<password> | Password (visible in development only) |
ENTITY | Optional. The entity that generalizes System.User (e.g., Administration.Account). If omitted, the system auto-detects the unique System.User subtype. |
<UserRole> | One or more project-level user roles to assign |
Examples
-- Basic demo user
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!' (Administrator);
-- With explicit entity
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!'
ENTITY Administration.Account (Administrator);
-- Multiple roles
CREATE DEMO USER 'demo_manager' PASSWORD 'Manager1!'
(Manager, Reporting);
-- Standard user
CREATE DEMO USER 'demo_user' PASSWORD 'User1234!' (Employee);
DROP DEMO USER
DROP DEMO USER '<username>';
Example:
DROP DEMO USER 'demo_admin';
Enabling Demo Users
Demo users only appear on the login screen when enabled in project security:
ALTER PROJECT SECURITY DEMO USERS ON;
To hide them:
ALTER PROJECT SECURITY DEMO USERS OFF;
Listing Demo Users
SHOW DEMO USERS;
Typical Setup
-- Enable demo users and set prototype security
ALTER PROJECT SECURITY LEVEL PROTOTYPE;
ALTER PROJECT SECURITY DEMO USERS ON;
-- Create demo accounts for each role
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!'
ENTITY Administration.Account (Administrator);
CREATE DEMO USER 'demo_user' PASSWORD 'User1234!'
ENTITY Administration.Account (Employee);
CREATE DEMO USER 'demo_guest' PASSWORD 'Guest123!'
ENTITY Administration.Account (Guest);
See Also
- Security – overview of the security model
- Module Roles and User Roles – defining the user roles assigned to demo users
- GRANT / REVOKE – complete permission management reference
Navigation and Settings
Navigation defines how users move through a Mendix application. Each device type has its own navigation profile with a home page, optional role-specific home pages, a login page, and a menu structure. Project settings control runtime behavior, configurations, languages, and workflows.
Navigation Overview
A Mendix application has one or more navigation profiles, each targeting a device type. MDL supports inspecting and replacing entire profiles.
Inspecting Navigation
-- Summary of all profiles
SHOW NAVIGATION;
-- Menu tree for a specific profile
SHOW NAVIGATION MENU Responsive;
-- Home page assignments across all profiles
SHOW NAVIGATION HOMES;
-- Full MDL output (round-trippable)
DESCRIBE NAVIGATION;
DESCRIBE NAVIGATION Responsive;
Navigation Profiles
| Profile | Description |
|---|---|
Responsive | Web browser (desktop, laptop) |
Tablet | Tablet browser |
Phone | Phone browser |
NativePhone | Native mobile (React Native) |
See Navigation Profiles for full syntax.
Settings Overview
Project settings control runtime configuration, database connections, languages, and workflow behavior.
-- Overview of all settings
SHOW SETTINGS;
-- Full MDL output
DESCRIBE SETTINGS;
See Project Settings for the ALTER SETTINGS syntax.
See Also
- Navigation Profiles – creating and replacing navigation profiles
- Home Pages and Menus – home page assignments and menu structures
- Project Settings – runtime, configuration, and language settings
Navigation Profiles
A navigation profile defines the navigation structure for a specific device type. Each profile has a default home page, optional role-specific home pages, an optional login page, and a menu.
Profile Types
| Profile | MDL Name | Description |
|---|---|---|
| Responsive web | Responsive | Default browser navigation |
| Tablet web | Tablet | Tablet-optimized browser navigation |
| Phone web | Phone | Phone-optimized browser navigation |
| Native mobile | NativePhone | React Native mobile navigation |
CREATE OR REPLACE NAVIGATION
Replaces an entire navigation profile:
CREATE OR REPLACE NAVIGATION <Profile>
HOME PAGE <Module>.<Page>
[HOME PAGE <Module>.<Page> FOR <Module>.<Role>]
[LOGIN PAGE <Module>.<Page>]
[NOT FOUND PAGE <Module>.<Page>]
[MENU (
<menu-items>
)]
Full Example
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
HOME PAGE MyModule.AdminHome FOR MyModule.Administrator
LOGIN PAGE Administration.Login
NOT FOUND PAGE MyModule.Custom404
MENU (
MENU ITEM 'Home' PAGE MyModule.Home_Web;
MENU ITEM 'Orders' PAGE Shop.Order_Overview;
MENU 'Administration' (
MENU ITEM 'Users' PAGE Administration.Account_Overview;
MENU ITEM 'Settings' PAGE MyModule.Settings;
);
);
Minimal Example
A profile only requires a home page:
CREATE OR REPLACE NAVIGATION Phone
HOME PAGE MyModule.Home_Phone;
DESCRIBE NAVIGATION
View the current navigation profile in MDL syntax:
-- All profiles
DESCRIBE NAVIGATION;
-- Single profile
DESCRIBE NAVIGATION Responsive;
The output is round-trippable – you can copy it, modify it, and execute it as a CREATE OR REPLACE NAVIGATION statement.
See Also
- Navigation and Settings – overview of navigation and settings
- Home Pages and Menus – details on home page and menu configuration
- Project Settings – runtime and configuration settings
Home Pages and Menus
Each navigation profile has a default home page, optional role-specific home pages, and a menu tree. These determine what users see when they first open the application and how they navigate between pages.
Home Pages
Default Home Page
Every profile requires a default home page. This is shown to users whose role has no specific home page assignment:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web;
Role-Specific Home Pages
Use HOME PAGE ... FOR to direct users to different pages based on their module role:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
HOME PAGE MyModule.AdminDashboard FOR MyModule.Administrator
HOME PAGE MyModule.ManagerDashboard FOR MyModule.Manager;
When a user logs in, the runtime checks their roles and redirects to the most specific matching home page. If no role-specific page matches, the default home page is used.
Login Page
The login page is shown to unauthenticated users:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
LOGIN PAGE Administration.Login;
Not Found Page
An optional custom 404 page:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
NOT FOUND PAGE MyModule.Custom404;
Menus
The MENU block defines the navigation menu as a tree of items and submenus.
Menu Items
A menu item links a label to a page:
MENU ITEM '<label>' PAGE <Module>.<Page>;
Submenus
Nest items inside a MENU '<label>' (...) block:
MENU '<label>' (
<menu-items>
);
Complete Example
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE Shop.Home
LOGIN PAGE Administration.Login
MENU (
MENU ITEM 'Home' PAGE Shop.Home;
MENU ITEM 'Products' PAGE Shop.Product_Overview;
MENU ITEM 'Orders' PAGE Shop.Order_Overview;
MENU 'Administration' (
MENU ITEM 'Users' PAGE Administration.Account_Overview;
MENU ITEM 'Roles' PAGE Administration.Role_Overview;
MENU 'System' (
MENU ITEM 'Logs' PAGE Administration.Log_Overview;
MENU ITEM 'Settings' PAGE Shop.Settings;
);
);
);
Inspecting Menus
-- View menu tree
SHOW NAVIGATION MENU;
SHOW NAVIGATION MENU Responsive;
-- View home page assignments
SHOW NAVIGATION HOMES;
See Also
- Navigation and Settings – overview of navigation concepts
- Navigation Profiles – profile types and CREATE OR REPLACE syntax
- Project Settings – runtime and configuration settings
Project Settings
Project settings control application runtime behavior, server configurations, language settings, and workflow configuration. MDL provides commands to inspect and modify all settings categories.
Inspecting Settings
-- Overview of all settings
SHOW SETTINGS;
-- Full MDL output (round-trippable)
DESCRIBE SETTINGS;
ALTER SETTINGS
Settings are organized into categories. Each ALTER SETTINGS command targets one category.
Model Settings
Runtime-level settings such as the after-startup microflow, hashing algorithm, and Java version:
ALTER SETTINGS MODEL <Key> = <Value>;
Examples:
ALTER SETTINGS MODEL AfterStartupMicroflow = 'MyModule.ACT_Startup';
ALTER SETTINGS MODEL HashAlgorithm = 'BCrypt';
ALTER SETTINGS MODEL JavaVersion = '17';
Configuration Settings
Server configuration settings like database type, URL, and HTTP port. Each configuration is identified by name (commonly 'default'):
ALTER SETTINGS CONFIGURATION '<Name>' <Key> = <Value>;
Examples:
ALTER SETTINGS CONFIGURATION 'default' DatabaseType = 'POSTGRESQL';
ALTER SETTINGS CONFIGURATION 'default' DatabaseUrl = 'jdbc:postgresql://localhost:5432/myapp';
ALTER SETTINGS CONFIGURATION 'default' HttpPortNumber = '8080';
Constant Overrides
Override a constant value within a specific configuration:
ALTER SETTINGS CONSTANT '<ConstantName>' VALUE '<value>' IN CONFIGURATION '<cfg>';
Example:
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://staging.example.com' IN CONFIGURATION 'default';
Language Settings
Set the default language for the application:
ALTER SETTINGS LANGUAGE <Key> = <Value>;
Example:
ALTER SETTINGS LANGUAGE DefaultLanguageCode = 'en_US';
Workflow Settings
Configure workflow behavior such as the user entity and task parallelism:
ALTER SETTINGS WORKFLOWS <Key> = <Value>;
Examples:
ALTER SETTINGS WORKFLOWS UserEntity = 'Administration.Account';
ALTER SETTINGS WORKFLOWS DefaultTaskParallelism = '5';
See Also
- Navigation and Settings – overview of navigation and settings
- Navigation Profiles – navigation profile configuration
- Workflows – workflow definitions
Workflows
Workflows model long-running, multi-step business processes such as approval chains, onboarding sequences, and review cycles. Unlike microflows, which execute synchronously in a single transaction, workflows persist their state and can pause indefinitely – waiting for user input, timers, or external notifications.
When to Use Workflows
Workflows are appropriate when a process:
- Involves human tasks that require user action at specific steps
- Spans hours, days, or weeks rather than completing instantly
- Has branching logic based on user decisions (approve / reject / escalate)
- Needs a visual overview of progress for administrators
For immediate, transactional logic, use microflows instead. See Workflow vs Microflow for a detailed comparison.
Inspecting Workflows
-- List all workflows
SHOW WORKFLOWS;
SHOW WORKFLOWS IN MyModule;
-- View full definition
DESCRIBE WORKFLOW MyModule.ApprovalFlow;
Quick Example
CREATE WORKFLOW Approval.RequestApproval
PARAMETER $Context: Approval.Request
OVERVIEW PAGE Approval.WorkflowOverview
BEGIN
USER TASK ReviewTask 'Review the request'
PAGE Approval.ReviewPage
OUTCOMES 'Approve' {
CALL MICROFLOW Approval.ACT_Approve;
} 'Reject' {
CALL MICROFLOW Approval.ACT_Reject;
};
END WORKFLOW;
See Also
- Workflow Structure – full CREATE WORKFLOW syntax
- Activity Types – all workflow activity types
- Workflow vs Microflow – choosing between the two
- GRANT / REVOKE – granting execute access on workflows
Workflow Structure
A workflow definition consists of a context parameter, an optional overview page, and a sequence of activities. Activities execute in order unless control flow elements (decisions, parallel splits, jumps) alter the sequence.
CREATE WORKFLOW
CREATE [OR MODIFY] WORKFLOW <Module>.<Name>
PARAMETER $<Name>: <Module>.<Entity>
[OVERVIEW PAGE <Module>.<Page>]
BEGIN
<activities>
END WORKFLOW;
| Element | Description |
|---|---|
PARAMETER | The workflow context object, passed when the workflow is started |
OVERVIEW PAGE | Optional page shown in the workflow admin dashboard |
| Activities | A sequence of user tasks, microflow calls, decisions, etc. |
Full Example
CREATE WORKFLOW HR.OnboardEmployee
PARAMETER $Context: HR.OnboardingRequest
OVERVIEW PAGE HR.OnboardingOverview
BEGIN
-- Parallel: IT setup and HR paperwork happen simultaneously
PARALLEL SPLIT
PATH 1 {
USER TASK SetupLaptop 'Set up laptop and accounts'
PAGE HR.IT_SetupPage
OUTCOMES 'Done' { };
}
PATH 2 {
USER TASK SignDocuments 'Sign employment documents'
PAGE HR.DocumentSignPage
OUTCOMES 'Signed' { };
};
-- After both paths complete, manager reviews
DECISION 'Manager approval required?'
OUTCOMES 'Yes' {
USER TASK ManagerReview 'Review onboarding'
PAGE HR.ManagerReviewPage
OUTCOMES 'Approve' { } 'Reject' {
CALL MICROFLOW HR.ACT_RejectOnboarding;
END;
};
} 'No' { };
CALL MICROFLOW HR.ACT_CompleteOnboarding;
END WORKFLOW;
DROP WORKFLOW
DROP WORKFLOW HR.OnboardEmployee;
Workflow Access
Grant or revoke execute access to control who can start a workflow:
GRANT EXECUTE ON WORKFLOW HR.OnboardEmployee TO HR.Manager, HR.Admin;
REVOKE EXECUTE ON WORKFLOW HR.OnboardEmployee FROM HR.Manager;
See Also
- Workflows – overview and when to use workflows
- Activity Types – details on each activity type
- Workflow vs Microflow – choosing the right construct
Activity Types
Workflow activities are the building blocks of a workflow definition. Each activity type serves a different purpose, from waiting for user input to calling microflows or branching execution.
User Task
A user task pauses the workflow until a user completes it. Each outcome resumes the workflow down a different path.
USER TASK <name> '<caption>'
[PAGE <Module>.<Page>]
[TARGETING MICROFLOW <Module>.<Microflow>]
OUTCOMES '<outcome>' { <activities> } ['<outcome>' { <activities> }] ...;
| Element | Description |
|---|---|
<name> | Internal activity name |
<caption> | Display label shown to users |
PAGE | The page opened when the user acts on the task |
TARGETING MICROFLOW | Microflow that determines which users see the task |
OUTCOMES | Named outcomes, each with a block of follow-up activities |
Example:
USER TASK ReviewTask 'Review the request'
PAGE Approval.ReviewPage
TARGETING MICROFLOW Approval.ACT_GetReviewers
OUTCOMES 'Approve' {
CALL MICROFLOW Approval.ACT_Approve;
} 'Reject' {
CALL MICROFLOW Approval.ACT_Reject;
END;
};
Call Microflow
Execute a microflow as part of the workflow. Optionally specify a comment and outcomes:
CALL MICROFLOW <Module>.<Name> [COMMENT '<text>']
[OUTCOMES '<outcome>' { <activities> } ...];
Example:
CALL MICROFLOW HR.ACT_SendNotification COMMENT 'Notify the applicant';
Call Workflow
Start a sub-workflow:
CALL WORKFLOW <Module>.<Name> [COMMENT '<text>'];
Example:
CALL WORKFLOW HR.BackgroundCheck COMMENT 'Run background check sub-process';
Decision
Branch the workflow based on a condition. Each outcome contains a block of activities:
DECISION ['<caption>']
OUTCOMES '<outcome>' { <activities> } ['<outcome>' { <activities> }] ...;
Example:
DECISION 'Order value over $1000?'
OUTCOMES 'Yes' {
USER TASK ManagerApproval 'Manager must approve'
PAGE Shop.ApprovalPage
OUTCOMES 'Approved' { } 'Rejected' { END; };
} 'No' { };
Parallel Split
Execute multiple paths concurrently. The workflow continues after all paths complete:
PARALLEL SPLIT
PATH 1 { <activities> }
PATH 2 { <activities> }
[PATH 3 { <activities> }] ...;
Example:
PARALLEL SPLIT
PATH 1 {
USER TASK LegalReview 'Legal review'
PAGE Legal.ReviewPage
OUTCOMES 'Approved' { };
}
PATH 2 {
USER TASK FinanceReview 'Finance review'
PAGE Finance.ReviewPage
OUTCOMES 'Approved' { };
};
Jump To
Jump to a named activity elsewhere in the workflow (creates a loop or skip):
JUMP TO <activity-name>;
Example:
JUMP TO ReviewTask;
Wait for Timer
Pause the workflow until a timer expression evaluates:
WAIT FOR TIMER ['<expression>'];
Example:
WAIT FOR TIMER 'addDays([%CurrentDateTime%], 3)';
Wait for Notification
Pause the workflow until an external notification resumes it:
WAIT FOR NOTIFICATION;
End
Terminate the current workflow path:
END;
Typically used inside an outcome block to stop the workflow after a rejection or cancellation.
Summary Table
| Activity | Purpose | Pauses Workflow? |
|---|---|---|
USER TASK | Wait for human action | Yes |
CALL MICROFLOW | Execute server logic | No |
CALL WORKFLOW | Start sub-workflow | Depends on sub-workflow |
DECISION | Branch on condition | No |
PARALLEL SPLIT | Concurrent execution | Yes (waits for all paths) |
JUMP TO | Go to named activity | No |
WAIT FOR TIMER | Delay execution | Yes |
WAIT FOR NOTIFICATION | Wait for external signal | Yes |
END | Terminate path | N/A |
See Also
- Workflows – overview and when to use workflows
- Workflow Structure – CREATE WORKFLOW syntax
- Workflow vs Microflow – choosing between workflows and microflows
Workflow vs Microflow
Workflows and microflows are both used to express business logic, but they serve different purposes and have different execution models.
Comparison
| Aspect | Microflow | Workflow |
|---|---|---|
| Duration | Milliseconds to seconds | Hours, days, or weeks |
| Execution | Synchronous, single transaction | Asynchronous, persisted state |
| Trigger | Button click, page load, event | Explicitly started, resumes on events |
| Human tasks | Not supported (shows pages but does not wait) | Built-in user task with outcomes |
| Branching | IF / ELSE within a single execution | Decisions with separate outcome paths |
| Parallelism | Not supported | Parallel splits with multiple concurrent paths |
| Persistence | No state between calls | State persisted across restarts |
| Monitoring | Log messages only | Admin overview page, task inbox |
| Error handling | ON ERROR blocks | Outcome-based routing |
When to Use a Microflow
Use a microflow when:
- The operation completes immediately (create, update, delete, calculate)
- No human decision points are needed
- The logic runs within a single database transaction
- You need fine-grained error handling with rollback
CREATE MICROFLOW Shop.ACT_ProcessPayment
BEGIN
DECLARE $Order Shop.Order;
RETRIEVE $Order FROM Shop.Order WHERE [OrderId = $OrderId] LIMIT 1;
CHANGE $Order (Status = 'Paid', PaidDate = [%CurrentDateTime%]);
COMMIT $Order;
RETURN true;
END;
When to Use a Workflow
Use a workflow when:
- The process involves human approval or review steps
- The process spans multiple days or longer
- You need a visual overview of where each case stands
- Multiple steps should happen in parallel
- The process may wait for external events
CREATE WORKFLOW Shop.OrderApproval
PARAMETER $Context: Shop.Order
OVERVIEW PAGE Shop.OrderWorkflowOverview
BEGIN
USER TASK ReviewOrder 'Review the order'
PAGE Shop.OrderReviewPage
OUTCOMES 'Approve' {
CALL MICROFLOW Shop.ACT_ApproveOrder;
} 'Reject' {
CALL MICROFLOW Shop.ACT_RejectOrder;
END;
};
CALL MICROFLOW Shop.ACT_FulfillOrder;
END WORKFLOW;
Combining Both
Workflows and microflows work together. A workflow orchestrates the process while microflows handle the actual logic at each step:
- Workflow defines the process: who needs to act, in what order, with what outcomes
- Microflows execute within workflow steps: create objects, send emails, update status
- Pages provide the UI for user tasks
A common pattern is:
-- Microflow does the work
CREATE MICROFLOW Approval.ACT_Approve
BEGIN
DECLARE $Request Approval.Request;
RETRIEVE $Request FROM Approval.Request WHERE [id = $RequestId] LIMIT 1;
CHANGE $Request (Status = 'Approved', ApprovedBy = '[%CurrentUser%]');
COMMIT $Request;
END;
-- Workflow orchestrates the process
CREATE WORKFLOW Approval.ApprovalProcess
PARAMETER $Context: Approval.Request
BEGIN
USER TASK Review 'Review request'
PAGE Approval.ReviewPage
OUTCOMES 'Approve' {
CALL MICROFLOW Approval.ACT_Approve;
} 'Reject' {
CALL MICROFLOW Approval.ACT_Reject;
END;
};
END WORKFLOW;
See Also
- Workflows – workflow overview
- Workflow Structure – CREATE WORKFLOW syntax
- Activity Types – workflow activity reference
- Microflows and Nanoflows – microflow overview
Business Events
Business events enable event-driven integration between Mendix applications. An application can publish events when something happens (e.g., an order is placed) and other applications can subscribe to those events and react accordingly.
Business events are organized into event services, each of which defines one or more messages with typed attributes.
Inspecting Business Events
-- List all business event services
SHOW BUSINESS EVENTS;
SHOW BUSINESS EVENTS IN MyModule;
-- View full definition
DESCRIBE BUSINESS EVENT SERVICE MyModule.OrderEvents;
Quick Example
CREATE BUSINESS EVENT SERVICE Shop.OrderEvents (
Version: '1.0',
Description: 'Events related to order lifecycle'
) {
MESSAGE OrderPlaced (
OrderId: String,
CustomerName: String,
TotalAmount: Decimal,
OrderDate: DateTime
)
MESSAGE OrderShipped (
OrderId: String,
TrackingNumber: String,
ShippedDate: DateTime
)
};
See Also
- Event Services – CREATE and DROP syntax for event services
- Publishing and Consuming Events – event publishing and consumption patterns
Event Services
An event service is a named container for business event messages. It defines the contract between publishers and consumers: what events exist and what data each event carries.
CREATE BUSINESS EVENT SERVICE
CREATE BUSINESS EVENT SERVICE <Module>.<Name> (
[Version: '<version>',]
[Description: '<text>']
) {
MESSAGE <MessageName> (
<AttributeName>: <Type> [, ...]
)
[MESSAGE <MessageName> ( ... )]
};
| Element | Description |
|---|---|
Version | Service version string |
Description | Human-readable description of the service |
MESSAGE | A named event type with typed attributes |
Attribute Types
Message attributes support standard Mendix types:
| Type | Description |
|---|---|
String | Text value |
Integer | 32-bit integer |
Long | 64-bit integer |
Decimal | Precise decimal number |
Boolean | True or false |
DateTime | Date and time value |
Example
CREATE BUSINESS EVENT SERVICE HR.EmployeeEvents (
Version: '2.0',
Description: 'Employee lifecycle events'
) {
MESSAGE EmployeeHired (
EmployeeId: String,
FullName: String,
Department: String,
StartDate: DateTime
)
MESSAGE EmployeeTerminated (
EmployeeId: String,
TerminationDate: DateTime,
Reason: String
)
};
DROP BUSINESS EVENT SERVICE
Remove an event service:
DROP BUSINESS EVENT SERVICE HR.EmployeeEvents;
Listing and Inspecting
-- List all services
SHOW BUSINESS EVENTS;
-- Filter by module
SHOW BUSINESS EVENTS IN HR;
-- Full MDL definition
DESCRIBE BUSINESS EVENT SERVICE HR.EmployeeEvents;
The DESCRIBE output is round-trippable – it can be copied, modified, and executed as a CREATE statement.
See Also
- Business Events – overview of business events
- Publishing and Consuming Events – event publishing and consumption patterns
Publishing and Consuming Events
Business events follow a publish-subscribe pattern. One application publishes events when significant actions occur, and other applications consume those events to trigger their own logic.
Publishing Events
A publishing application defines a business event service with its message types:
CREATE BUSINESS EVENT SERVICE Shop.OrderEvents (
Version: '1.0',
Description: 'Order lifecycle events'
) {
MESSAGE OrderPlaced (
OrderId: String,
CustomerName: String,
TotalAmount: Decimal,
OrderDate: DateTime
)
MESSAGE OrderCancelled (
OrderId: String,
Reason: String,
CancelledDate: DateTime
)
};
The event service acts as a contract. Consuming applications rely on the message structure, so changes to attribute names or types should be accompanied by a version increment.
Consuming Events
A consuming application subscribes to events from another application’s event service. When an event is received, a configured microflow is triggered to handle it.
The consumption side is typically configured in Mendix Studio Pro by selecting the published event service and mapping each message to a handler microflow. The handler microflow receives the event attributes as parameters.
Handler Pattern
A typical handler microflow processes the incoming event data:
CREATE MICROFLOW Warehouse.ACT_HandleOrderPlaced
BEGIN
-- Parameters: $OrderId (String), $CustomerName (String), etc.
DECLARE $ShipmentRequest Warehouse.ShipmentRequest;
$ShipmentRequest = CREATE Warehouse.ShipmentRequest (
ExternalOrderId = $OrderId,
CustomerName = $CustomerName,
Status = 'Pending'
);
COMMIT $ShipmentRequest;
END;
Versioning
Event services include a version string to track contract changes:
CREATE BUSINESS EVENT SERVICE Shop.OrderEvents (
Version: '2.0',
Description: 'Order events - v2 adds ShippingAddress'
) {
MESSAGE OrderPlaced (
OrderId: String,
CustomerName: String,
TotalAmount: Decimal,
OrderDate: DateTime,
ShippingAddress: String
)
};
When adding new attributes, increment the version so consuming applications know to update their handlers.
Use Cases
| Scenario | Publisher | Consumer |
|---|---|---|
| Order fulfillment | Shop app publishes OrderPlaced | Warehouse app creates shipment request |
| Customer sync | CRM app publishes CustomerUpdated | Billing app updates customer record |
| Compliance | HR app publishes EmployeeTerminated | IT app revokes access |
| Notifications | Any app publishes events | Notification service sends emails/SMS |
See Also
- Business Events – overview of business events
- Event Services – CREATE and DROP syntax
Image Collections
Image collections are Mendix’s way of bundling images (icons, logos, graphics) within a module. Each collection can contain multiple images in various formats (PNG, SVG, GIF, JPEG, BMP, WebP).
Inspecting Image Collections
-- List all image collections across all modules
SHOW IMAGE COLLECTION;
-- Filter by module
SHOW IMAGE COLLECTION IN MyModule;
-- View full definition including embedded images
DESCRIBE IMAGE COLLECTION MyModule.AppIcons;
The DESCRIBE output includes the full CREATE statement. If the collection contains images, they are shown with IMAGE 'name' FROM FILE 'path' syntax that can be copied and re-executed. In the TUI, images are rendered inline when the terminal supports it (Kitty, iTerm2, Sixel).
CREATE IMAGE COLLECTION
CREATE IMAGE COLLECTION <Module>.<Name>
[EXPORT LEVEL 'Hidden'|'Public']
[COMMENT '<description>']
[(
IMAGE <Name> FROM FILE '<path>',
...
)];
| Option | Description | Default |
|---|---|---|
EXPORT LEVEL | 'Hidden' (internal to module) or 'Public' (accessible from other modules) | 'Hidden' |
COMMENT | Documentation for the collection | (none) |
IMAGE Name FROM FILE | Load an image from the filesystem into the collection | (none) |
The image format is detected automatically from the file extension. Relative paths are resolved from the current working directory. Supported formats: PNG, SVG, GIF, JPEG, BMP, WebP.
Examples
-- Minimal: empty collection
CREATE IMAGE COLLECTION MyModule.AppIcons;
-- With export level
CREATE IMAGE COLLECTION MyModule.SharedIcons EXPORT LEVEL 'Public';
-- With comment
CREATE IMAGE COLLECTION MyModule.StatusIcons
COMMENT 'Icons for order and task status indicators';
-- With images from files
CREATE IMAGE COLLECTION MyModule.NavigationIcons (
IMAGE home FROM FILE 'assets/home.png',
IMAGE settings FROM FILE 'assets/settings.svg'
);
-- All options combined
CREATE IMAGE COLLECTION MyModule.BrandAssets
EXPORT LEVEL 'Public'
COMMENT 'Company branding assets' (
IMAGE logo_dark FROM FILE 'assets/logo-dark.png',
IMAGE logo_light FROM FILE 'assets/logo-light.png'
);
DROP IMAGE COLLECTION
Remove a collection and all its embedded images:
DROP IMAGE COLLECTION MyModule.StatusIcons;
See Also
- MDL Quick Reference – syntax summary table
Code Navigation
mxcli provides a suite of cross-reference navigation commands that let you explore relationships between elements in a Mendix project. These commands answer questions like “what calls this microflow?”, “what would break if I change this entity?”, and “what does this element depend on?”
Prerequisites
Cross-reference navigation commands require a full catalog refresh before use:
REFRESH CATALOG FULL
This populates the reference data needed for callers, callees, references, impact, and context queries. A basic REFRESH CATALOG only builds metadata tables and is not sufficient for cross-reference navigation.
Available Commands
| Command | Purpose |
|---|---|
SHOW CALLERS OF | Find what calls a given element |
SHOW CALLEES OF | Find what a given element calls |
SHOW REFERENCES OF | Find all references to and from an element |
SHOW IMPACT OF | Analyze the impact of changing an element |
SHOW CONTEXT OF | Show the surrounding context of an element |
SEARCH | Full-text search across all strings and source |
CLI Subcommands
These commands are also available as CLI subcommands for scripting and piping:
mxcli callers -p app.mpr Module.MyMicroflow
mxcli callees -p app.mpr Module.MyMicroflow
mxcli refs -p app.mpr Module.Customer
mxcli impact -p app.mpr Module.Customer
mxcli context -p app.mpr Module.MyMicroflow --depth 3
mxcli search -p app.mpr "validation"
Typical Workflow
- Refresh the catalog to ensure cross-reference data is current
- Explore references to understand how elements are connected
- Analyze impact before making changes
- Use context to gather surrounding information for AI-assisted development
-- Step 1: Build the catalog
REFRESH CATALOG FULL;
-- Step 2: Understand what uses a microflow
SHOW CALLERS OF Sales.ACT_ProcessOrder;
-- Step 3: Check impact before renaming an entity
SHOW IMPACT OF Sales.Customer;
-- Step 4: Gather context for AI consumption
SHOW CONTEXT OF Sales.SubmitOrder;
How AI Assistants Use This
When you ask an AI assistant to modify an element, it uses these commands to:
- Discover dependencies with
SHOW CALLERSandSHOW REFERENCES - Assess risk with
SHOW IMPACTbefore making changes - Gather context with
SHOW CONTEXTfor informed code generation - Find related elements with
SEARCHto understand patterns
SHOW CALLERS / CALLEES
These commands trace the call graph of your Mendix project, showing what calls a given element and what that element calls.
Prerequisites
Both commands require a full catalog refresh:
REFRESH CATALOG FULL;
SHOW CALLERS OF
Shows all elements that call or reference a given element.
Syntax:
SHOW CALLERS OF <qualified-name>
Examples:
-- Find everything that calls a microflow
SHOW CALLERS OF Sales.ACT_ProcessOrder;
-- Find what references an entity
SHOW CALLERS OF Sales.Customer;
-- Find what uses a page
SHOW CALLERS OF Sales.CustomerOverview;
CLI Usage
# Basic caller lookup
mxcli callers -p app.mpr Module.MyMicroflow
# Transitive callers (full call chain)
mxcli callers -p app.mpr Module.MyMicroflow --transitive
The --transitive flag follows the call chain recursively, showing not just direct callers but also callers of callers.
SHOW CALLEES OF
Shows all elements that a given element calls or references.
Syntax:
SHOW CALLEES OF <qualified-name>
Examples:
-- Find what a microflow calls
SHOW CALLEES OF Sales.ACT_ProcessOrder;
-- Find what entities a microflow uses
SHOW CALLEES OF Sales.SubmitOrder;
CLI Usage
mxcli callees -p app.mpr Module.MyMicroflow
Use Cases
Before Refactoring
Check what calls a microflow before renaming or modifying it:
SHOW CALLERS OF Sales.ACT_OldName;
-- Review the list of callers before making changes
Understanding Dependencies
Trace the full call chain of a complex microflow:
SHOW CALLEES OF Sales.ACT_ProcessOrder;
-- Shows: Sales.ValidateOrder, Sales.UpdateInventory, Sales.SendConfirmation
Finding Dead Code
If SHOW CALLERS returns no results, the element may be unused:
SHOW CALLERS OF Sales.ACT_UnusedMicroflow;
-- Empty result = potentially dead code
SHOW REFERENCES / IMPACT
These commands provide broader reference tracking and impact analysis, helping you understand how changes propagate through a project.
Prerequisites
Both commands require a full catalog refresh:
REFRESH CATALOG FULL;
SHOW REFERENCES OF
Shows all references to and from a given element, combining both incoming and outgoing relationships.
Syntax:
SHOW REFERENCES OF <qualified-name>
Examples:
-- Find all references to/from an entity
SHOW REFERENCES OF Sales.Customer;
-- Find all references to/from a microflow
SHOW REFERENCES OF Sales.ACT_ProcessOrder;
-- Find all references to/from a page
SHOW REFERENCES OF Sales.CustomerOverview;
CLI Usage
mxcli refs -p app.mpr Module.Customer
Difference from CALLERS/CALLEES
While SHOW CALLERS and SHOW CALLEES focus on the call graph direction, SHOW REFERENCES combines both directions into a single view. It shows every element that either references or is referenced by the target element.
SHOW IMPACT OF
Performs impact analysis on an element, showing what would be affected if you changed or removed it.
Syntax:
SHOW IMPACT OF <qualified-name>
Examples:
-- Analyze impact of changing an entity
SHOW IMPACT OF Sales.Customer;
-- Analyze impact before removing a microflow
SHOW IMPACT OF Sales.ACT_CalculateTotal;
-- Check impact before moving an element
SHOW IMPACT OF Sales.CustomerOverview;
CLI Usage
mxcli impact -p app.mpr Module.Customer
Use Cases
Pre-Change Impact Assessment
Before modifying an entity, check what would be affected:
-- What depends on the Customer entity?
SHOW IMPACT OF Sales.Customer;
-- Review results: pages, microflows, associations, access rules
-- Then decide if the change is safe to make
Before Moving Elements
Moving elements across modules changes their qualified name and can break references:
-- Check impact before moving
SHOW IMPACT OF OldModule.Customer;
-- If impact is acceptable, proceed
MOVE ENTITY OldModule.Customer TO NewModule;
Finding All Usages of an Enumeration
SHOW REFERENCES OF Sales.OrderStatus;
-- Shows: entities with attributes of this type, microflows that use it, pages that display it
Dependency Mapping
Understand the full dependency web of a complex module:
SHOW REFERENCES OF Sales.Order;
SHOW REFERENCES OF Sales.OrderLine;
SHOW REFERENCES OF Sales.Order_Customer;
SHOW CONTEXT
The SHOW CONTEXT command assembles the surrounding context of an element within its module and project hierarchy. This is particularly useful for AI-assisted development, where an LLM needs to understand the neighborhood of an element to generate accurate code.
Prerequisites
Requires a full catalog refresh:
REFRESH CATALOG FULL;
Syntax
SHOW CONTEXT OF <qualified-name>
Examples
-- Show context of a microflow
SHOW CONTEXT OF Sales.ACT_ProcessOrder;
-- Show context of an entity
SHOW CONTEXT OF Sales.Customer;
-- Show context of a page
SHOW CONTEXT OF Sales.CustomerOverview;
CLI Usage
# Basic context retrieval
mxcli context -p app.mpr Module.MyMicroflow
# With depth control
mxcli context -p app.mpr Module.MyMicroflow --depth 3
The --depth flag controls how many levels of related elements are included in the context output. A higher depth gives a broader picture but produces more output.
What Context Includes
The context command gathers information about:
- The element’s own definition (MDL source)
- Elements that call it (callers)
- Elements it calls (callees)
- Related entities and associations
- Pages that display or edit the same data
- The module structure surrounding the element
Use Case: AI-Assisted Development
When an AI assistant needs to modify a microflow, it uses SHOW CONTEXT to understand:
- What parameters the microflow expects
- What entities and associations are involved
- What other microflows it interacts with
- What pages display the same data
This information allows the AI to generate code that fits naturally into the existing project structure.
# Gather context for an AI prompt
mxcli context -p app.mpr Sales.ACT_ProcessOrder --depth 2
Full-Text Search
The SEARCH command performs full-text search across all strings and source code in a Mendix project. It searches element names, documentation, captions, expressions, and other string content.
Syntax
SEARCH '<keyword>'
Examples
-- Search for a keyword
SEARCH 'validation';
-- Search for an entity name
SEARCH 'Customer';
-- Search for error messages
SEARCH 'cannot be empty';
-- Search for specific patterns
SEARCH 'CurrentDateTime';
CLI Usage
# Basic search
mxcli search -p app.mpr "validation"
# Quiet mode (no headers/formatting), suitable for piping
mxcli search -p app.mpr "validation" -q
# Output as names (type<TAB>name per line)
mxcli search -p app.mpr "validation" -q --format names
# Output as JSON array
mxcli search -p app.mpr "validation" -q --format json
Output Formats
| Format | Description |
|---|---|
names (default) | type<TAB>name per line, suitable for piping |
json | JSON array of search results |
The -q (quiet) flag suppresses headers and formatting, making output suitable for piping to other commands.
Piping Search Results
Search results can be piped to other mxcli commands for powerful workflows:
# Find the first microflow matching "error" and describe it
mxcli search -p app.mpr "error" -q --format names | head -1 | awk '{print $2}' | \
xargs mxcli describe -p app.mpr microflow
What Gets Searched
The search command looks through:
- Element names (entities, microflows, pages, etc.)
- Documentation comments
- Attribute names and captions
- Expression strings in microflows
- Widget captions and labels
- Enumeration value captions
- Log message strings
- Validation feedback messages
Comparison with Catalog Queries
SEARCH provides quick keyword-based search, while catalog queries (SELECT FROM CATALOG.*) provide structured SQL-based queries with filtering, joining, and aggregation. Use SEARCH for quick discovery and catalog queries for precise analysis.
-- Quick search: find anything mentioning "Customer"
SEARCH 'Customer';
-- Precise query: find entities named Customer with their module
SELECT Name, ModuleName FROM CATALOG.ENTITIES WHERE Name LIKE '%Customer%';
Catalog Queries
The catalog is a SQLite-based query system that provides SQL access to project metadata. It indexes all elements in your Mendix project – entities, microflows, pages, associations, attributes, and more – into queryable tables that support standard SQL syntax.
How It Works
The catalog builds an in-memory SQLite database from the project’s MPR file. This database is cached in .mxcli/catalog.db next to the MPR file for fast subsequent access.
There are two levels of catalog refresh:
| Command | What It Builds | Use Case |
|---|---|---|
REFRESH CATALOG | Basic metadata tables (entities, microflows, pages, etc.) | Quick queries about project structure |
REFRESH CATALOG FULL | Metadata + cross-references + source content | Code navigation, impact analysis, full-text search |
Quick Start
-- Build the catalog
REFRESH CATALOG;
-- See what tables are available
SHOW CATALOG TABLES;
-- Query entities in a module
SELECT Name, EntityType FROM CATALOG.ENTITIES WHERE ModuleName = 'Sales';
-- Find microflows that reference an entity
SELECT Name, ModuleName FROM CATALOG.MICROFLOWS
WHERE Description LIKE '%Customer%' OR Name LIKE '%Customer%';
Key Features
- Standard SQL syntax – SELECT, WHERE, JOIN, GROUP BY, ORDER BY, HAVING, UNION
- Multiple tables – Entities, attributes, associations, microflows, pages, and more
- Cross-reference data – Available after
REFRESH CATALOG FULL - Cached for performance – Stored in
.mxcli/catalog.dbnext to the MPR file - AI-friendly – Structured data that AI assistants can query programmatically
CLI Usage
# Refresh catalog
mxcli -p app.mpr -c "REFRESH CATALOG"
mxcli -p app.mpr -c "REFRESH CATALOG FULL"
# Query catalog
mxcli -p app.mpr -c "SELECT Name FROM CATALOG.MICROFLOWS LIMIT 10"
mxcli -p app.mpr -c "SHOW CATALOG TABLES"
Related Pages
- REFRESH CATALOG – Building and refreshing the catalog
- Available Tables – What tables are available
- SQL Queries – Query syntax and examples
- Use Cases – Practical analysis examples
REFRESH CATALOG
The REFRESH CATALOG command builds or rebuilds the catalog index from the current project state. The catalog must be refreshed before running catalog queries or cross-reference navigation commands.
Syntax
-- Basic refresh: builds metadata tables
REFRESH CATALOG
-- Full refresh: builds metadata + cross-references + source
REFRESH CATALOG FULL
-- Force rebuild: ignores cached catalog
REFRESH CATALOG FULL FORCE
Refresh Levels
Basic Refresh
REFRESH CATALOG;
Builds the core metadata tables: entities, attributes, associations, microflows, pages, enumerations, and other element types. This is sufficient for structural queries like “list all entities in a module” or “find pages with a specific URL.”
Full Refresh
REFRESH CATALOG FULL;
Builds everything in the basic refresh plus:
- Cross-reference data – Which elements reference which other elements
- Source content – Microflow activity content, expression strings, widget properties
- Call graph – Caller/callee relationships between elements
This level is required for:
SHOW CALLERS OFSHOW CALLEES OFSHOW REFERENCES OFSHOW IMPACT OFSHOW CONTEXT OFSEARCH- Full-text catalog queries
Force Rebuild
REFRESH CATALOG FULL FORCE;
Ignores the cached .mxcli/catalog.db file and rebuilds from scratch. Use this when the catalog appears stale or corrupt.
CLI Usage
# Quick refresh
mxcli -p app.mpr -c "REFRESH CATALOG"
# Full refresh with cross-references
mxcli -p app.mpr -c "REFRESH CATALOG FULL"
# Force rebuild
mxcli -p app.mpr -c "REFRESH CATALOG FULL FORCE"
Caching
The catalog is cached in .mxcli/catalog.db next to the MPR file. On subsequent refreshes, mxcli checks whether the project has changed and only rebuilds if necessary (unless FORCE is specified).
When to Refresh
- After opening a project – Run
REFRESH CATALOGto enable queries - Before cross-reference navigation – Run
REFRESH CATALOG FULLto enable CALLERS/CALLEES/IMPACT - After making changes – Refresh to update the catalog with new elements
- If results seem stale – Use
REFRESH CATALOG FULL FORCEto force a rebuild
Available Tables
The catalog provides several tables that can be queried using standard SQL syntax. Use SHOW CATALOG TABLES to list all available tables in your catalog.
Discovering Tables
SHOW CATALOG TABLES;
Core Tables
CATALOG.ENTITIES
Information about all entities in the project.
| Column | Description |
|---|---|
Id | Unique identifier |
Name | Entity name |
ModuleName | Module containing the entity |
QualifiedName | Full qualified name (Module.Entity) |
EntityType | Persistent, Non-Persistent, View, External |
Description | Documentation text |
AttributeCount | Number of attributes |
AccessRuleCount | Number of access rules defined |
SELECT Name, EntityType, AttributeCount
FROM CATALOG.ENTITIES
WHERE ModuleName = 'Sales'
ORDER BY Name;
CATALOG.ATTRIBUTES
Information about entity attributes.
| Column | Description |
|---|---|
Id | Unique identifier |
Name | Attribute name |
EntityId | Parent entity ID |
AttributeType | String, Integer, Decimal, Boolean, DateTime, etc. |
SELECT a.Name, a.AttributeType
FROM CATALOG.ATTRIBUTES a
JOIN CATALOG.ENTITIES e ON a.EntityId = e.Id
WHERE e.QualifiedName = 'Sales.Customer';
CATALOG.ASSOCIATIONS
Information about entity associations.
| Column | Description |
|---|---|
Id | Unique identifier |
Name | Association name |
ParentEntity | Parent (FROM) entity qualified name |
ChildEntity | Child (TO) entity qualified name |
AssociationType | Reference or ReferenceSet |
SELECT Name, ParentEntity, ChildEntity, AssociationType
FROM CATALOG.ASSOCIATIONS
WHERE ParentEntity LIKE '%Order%' OR ChildEntity LIKE '%Order%';
CATALOG.MICROFLOWS
Information about microflows and nanoflows.
| Column | Description |
|---|---|
Id | Unique identifier |
Name | Microflow name |
ModuleName | Module containing the microflow |
QualifiedName | Full qualified name |
ReturnType | Return type of the microflow |
Description | Documentation text |
Parameters | Parameter information |
ObjectUsage | Entities used in the microflow |
SELECT Name, ReturnType, Description
FROM CATALOG.MICROFLOWS
WHERE ModuleName = 'Sales'
ORDER BY Name;
CATALOG.PAGES
Information about pages and their properties.
| Column | Description |
|---|---|
Id | Unique identifier |
Name | Page name |
ModuleName | Module containing the page |
QualifiedName | Full qualified name |
URL | Page URL if configured |
DataSource | Primary data source |
WidgetTypes | Types of widgets used |
SELECT Name, URL, DataSource
FROM CATALOG.PAGES
WHERE ModuleName = 'Sales'
ORDER BY Name;
CATALOG.ACCESS_RULES
Information about entity access rules (available after full refresh).
| Column | Description |
|---|---|
Id | Unique identifier |
EntityId | Entity this rule applies to |
UserRole | Role this rule grants access to |
AllowRead | Whether read access is granted |
AllowWrite | Whether write access is granted |
SELECT e.QualifiedName, ar.UserRole, ar.AllowRead, ar.AllowWrite
FROM CATALOG.ACCESS_RULES ar
JOIN CATALOG.ENTITIES e ON ar.EntityId = e.Id
WHERE e.ModuleName = 'Sales';
Listing All Tables
To see the complete list of available tables in your catalog (which may vary by project and refresh level):
SHOW CATALOG TABLES;
mxcli -p app.mpr -c "SHOW CATALOG TABLES"
SQL Queries
The catalog supports standard SQL SELECT syntax for querying project metadata. Queries use the CATALOG. prefix to identify catalog tables.
Syntax
SELECT <columns> FROM CATALOG.<table> [WHERE <condition>] [ORDER BY <column>] [LIMIT <n>]
Standard SQL features are supported: SELECT, WHERE, JOIN, GROUP BY, ORDER BY, HAVING, UNION, LIMIT, aggregate functions (COUNT, SUM, AVG, MIN, MAX), and subqueries.
Basic Queries
Listing Elements
-- All entities in a module
SELECT Name, EntityType, Description
FROM CATALOG.ENTITIES
WHERE ModuleName = 'Sales';
-- All microflows in a module
SELECT Name, ReturnType
FROM CATALOG.MICROFLOWS
WHERE ModuleName = 'Sales';
-- Pages with URLs
SELECT Name, ModuleName, URL
FROM CATALOG.PAGES
WHERE URL IS NOT NULL;
Filtering with LIKE
-- Find entities by name pattern
SELECT QualifiedName FROM CATALOG.ENTITIES
WHERE Name LIKE '%Customer%';
-- Find microflows referencing an entity
SELECT Name, ModuleName
FROM CATALOG.MICROFLOWS
WHERE Description LIKE '%Customer%'
OR Name LIKE '%Customer%';
-- Find pages by URL pattern
SELECT Name, ModuleName, URL
FROM CATALOG.PAGES
WHERE URL LIKE '/admin%';
Joining Tables
-- Attributes of a specific entity
SELECT a.Name, a.AttributeType
FROM CATALOG.ATTRIBUTES a
JOIN CATALOG.ENTITIES e ON a.EntityId = e.Id
WHERE e.QualifiedName = 'Sales.Customer';
-- Entity relationships
SELECT Name, ParentEntity, ChildEntity, AssociationType
FROM CATALOG.ASSOCIATIONS
WHERE ParentEntity LIKE '%Order%'
OR ChildEntity LIKE '%Order%';
Aggregation
-- Count entities per module
SELECT ModuleName, COUNT(*) AS EntityCount
FROM CATALOG.ENTITIES
GROUP BY ModuleName
ORDER BY EntityCount DESC;
-- Documentation coverage by module
SELECT
ModuleName,
COUNT(*) AS TotalEntities,
SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) AS Documented,
ROUND(100.0 * SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) / COUNT(*), 1) AS CoveragePercent
FROM CATALOG.ENTITIES
GROUP BY ModuleName
ORDER BY CoveragePercent ASC;
Subqueries and UNION
-- Complete data flow for an entity
SELECT 'Page' AS Source, p.QualifiedName AS Element, 'displays' AS Action
FROM CATALOG.PAGES p
WHERE p.DataSource LIKE '%Customer%'
UNION ALL
SELECT 'Microflow' AS Source, m.QualifiedName AS Element, 'processes' AS Action
FROM CATALOG.MICROFLOWS m
WHERE m.ObjectUsage LIKE '%Customer%';
CLI Usage
# Run a catalog query
mxcli -p app.mpr -c "SELECT Name FROM CATALOG.ENTITIES WHERE ModuleName = 'Sales'"
# Query with limit
mxcli -p app.mpr -c "SELECT Name FROM CATALOG.MICROFLOWS LIMIT 10"
Notes
- Always run
REFRESH CATALOG(orREFRESH CATALOG FULL) before querying - Table and column names are case-sensitive in some contexts
- String comparisons use SQLite semantics (case-sensitive by default with
=, useLIKEfor case-insensitive matching) - The catalog is read-only; INSERT, UPDATE, and DELETE are not supported
Use Cases
This page provides practical catalog query examples for common analysis tasks: impact analysis, finding unused elements, detecting anti-patterns, and computing complexity metrics.
Duplicate Entity Detection
Find entities with similar names across modules that may be candidates for consolidation:
SELECT
e1.ModuleName AS Module1,
e1.Name AS Entity1,
e2.ModuleName AS Module2,
e2.Name AS Entity2
FROM CATALOG.ENTITIES e1
JOIN CATALOG.ENTITIES e2
ON LOWER(e1.Name) = LOWER(e2.Name)
AND e1.ModuleName < e2.ModuleName
ORDER BY e1.Name;
Detect copy-paste inheritance anti-patterns by finding entities with identical attribute patterns:
SELECT
e1.QualifiedName AS Entity1,
e2.QualifiedName AS Entity2,
COUNT(*) AS MatchingAttributes
FROM CATALOG.ATTRIBUTES a1
JOIN CATALOG.ATTRIBUTES a2
ON a1.Name = a2.Name
AND a1.AttributeType = a2.AttributeType
AND a1.EntityId != a2.EntityId
JOIN CATALOG.ENTITIES e1 ON a1.EntityId = e1.Id
JOIN CATALOG.ENTITIES e2 ON a2.EntityId = e2.Id
WHERE e1.QualifiedName < e2.QualifiedName
GROUP BY e1.QualifiedName, e2.QualifiedName
HAVING COUNT(*) >= 3
ORDER BY MatchingAttributes DESC;
Data Lineage Analysis
Entity Dependency Graph
Find all entities connected to a given entity via associations:
SELECT
'Outgoing' AS Direction,
a.Name AS Association,
a.ChildEntity AS RelatedEntity,
a.AssociationType AS Type
FROM CATALOG.ASSOCIATIONS a
WHERE a.ParentEntity = 'Sales.Order'
UNION ALL
SELECT
'Incoming' AS Direction,
a.Name AS Association,
a.ParentEntity AS RelatedEntity,
a.AssociationType AS Type
FROM CATALOG.ASSOCIATIONS a
WHERE a.ChildEntity = 'Sales.Order';
Microflows That Modify an Entity
Track all business logic touching specific data:
SELECT
m.QualifiedName AS Microflow,
m.ReturnType,
m.Description
FROM CATALOG.MICROFLOWS m
WHERE m.ObjectUsage LIKE '%Sales.Customer%'
OR m.Parameters LIKE '%Sales.Customer%'
OR m.ReturnType LIKE '%Sales.Customer%';
Page-to-Entity Mapping
Which pages display or edit which entities:
SELECT
p.QualifiedName AS Page,
p.DataSource AS Entity,
p.WidgetTypes
FROM CATALOG.PAGES p
WHERE p.DataSource IS NOT NULL
ORDER BY p.DataSource, p.QualifiedName;
Orphan Detection
Find persistent entities with no associations, which may indicate missing relationships:
SELECT e.QualifiedName, e.EntityType, e.AttributeCount
FROM CATALOG.ENTITIES e
LEFT JOIN CATALOG.ASSOCIATIONS a
ON e.QualifiedName = a.ParentEntity
OR e.QualifiedName = a.ChildEntity
WHERE a.Id IS NULL
AND e.EntityType = 'Persistent'
ORDER BY e.ModuleName, e.Name;
Complexity Analysis
Find entities with high complexity (many attributes and associations):
SELECT
e.QualifiedName,
e.AttributeCount,
COUNT(a.Id) AS AssociationCount,
e.AttributeCount + COUNT(a.Id) AS ComplexityScore
FROM CATALOG.ENTITIES e
LEFT JOIN CATALOG.ASSOCIATIONS a
ON e.QualifiedName = a.ParentEntity
OR e.QualifiedName = a.ChildEntity
GROUP BY e.QualifiedName, e.AttributeCount
HAVING ComplexityScore > 15
ORDER BY ComplexityScore DESC;
Documentation Coverage
Identify technical debt in documentation:
-- Entities without documentation
SELECT QualifiedName, EntityType, AttributeCount
FROM CATALOG.ENTITIES
WHERE (Description IS NULL OR Description = '')
ORDER BY AttributeCount DESC;
-- Documentation coverage by module
SELECT
ModuleName,
COUNT(*) AS TotalEntities,
SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) AS Documented,
ROUND(100.0 * SUM(CASE WHEN Description != '' THEN 1 ELSE 0 END) / COUNT(*), 1) AS CoveragePercent
FROM CATALOG.ENTITIES
GROUP BY ModuleName
ORDER BY CoveragePercent ASC;
Security Analysis
Entities Without Access Rules
SELECT
e.QualifiedName,
e.EntityType,
e.AccessRuleCount
FROM CATALOG.ENTITIES e
WHERE e.AccessRuleCount = 0
AND e.EntityType = 'Persistent'
ORDER BY e.ModuleName;
Wide-Open Entities
Review entities accessible to anonymous users:
SELECT
e.QualifiedName,
e.AttributeCount,
ar.UserRole,
ar.AllowRead,
ar.AllowWrite
FROM CATALOG.ENTITIES e
JOIN CATALOG.ACCESS_RULES ar ON e.Id = ar.EntityId
WHERE ar.AllowRead = 1
AND ar.UserRole = 'Anonymous'
ORDER BY e.AttributeCount DESC;
Module Health Dashboard
Overview of module complexity and documentation:
SELECT
ModuleName,
COUNT(DISTINCT e.Id) AS Entities,
COUNT(DISTINCT m.Id) AS Microflows,
COUNT(DISTINCT p.Id) AS Pages,
ROUND(100.0 * SUM(CASE WHEN e.Description != '' THEN 1 ELSE 0 END) /
NULLIF(COUNT(DISTINCT e.Id), 0), 1) AS DocCoverage
FROM CATALOG.ENTITIES e
LEFT JOIN CATALOG.MICROFLOWS m ON e.ModuleName = m.ModuleName
LEFT JOIN CATALOG.PAGES p ON e.ModuleName = p.ModuleName
GROUP BY ModuleName
ORDER BY Entities DESC;
Naming Convention Violations
Find entities not following PascalCase:
SELECT QualifiedName, Name
FROM CATALOG.ENTITIES
WHERE Name != UPPER(SUBSTR(Name, 1, 1)) || SUBSTR(Name, 2)
OR Name LIKE '%_%'
OR Name LIKE '% %';
Linting and Reports
mxcli includes an extensible linting framework that checks Mendix projects for best practice violations, security issues, naming conventions, and architectural anti-patterns. The framework combines built-in Go rules with extensible Starlark rules.
Overview
The linting system provides:
- 14 built-in Go rules – Fast, compiled rules for common issues
- 27 bundled Starlark rules – Extensible rules covering security, quality, architecture, design, and conventions
- Custom rule support – Write your own rules in Starlark
- Multiple output formats – Text, JSON, and SARIF for CI integration
- Scored reports – Best practices report with category breakdowns
Rule Categories
| Category | Prefix | Focus |
|---|---|---|
| MDL | MDL001-MDL007 | Naming conventions, empty microflows, domain model size |
| Security | SEC001-SEC009 | Access rules, password policy, demo users, PII exposure |
| Convention | CONV001-CONV017 | Best practice conventions, error handling |
| Quality | QUAL001-QUAL004 | Complexity, documentation, long microflows |
| Architecture | ARCH001-ARCH003 | Cross-module data, entity business keys |
| Design | DESIGN001 | Entity attribute count |
Quick Start
# Lint a project
mxcli lint -p app.mpr
# List available rules
mxcli lint -p app.mpr --list-rules
# JSON output for CI
mxcli lint -p app.mpr --format json
# Generate a scored report
mxcli report -p app.mpr
Related Pages
- Built-in Rules – The 14 built-in Go rules
- Starlark Rules – The 27 bundled Starlark rules
- Writing Custom Rules – How to write your own rules
- mxcli lint – Command-line usage
- mxcli report – Best practices report
Built-in Rules
mxcli ships with 14 built-in lint rules implemented in Go. These rules are fast and always available.
MDL Rules
| Rule | Description |
|---|---|
| MDL001 | Naming conventions – Checks entity, attribute, and microflow naming patterns |
| MDL002 | Empty microflows – Detects microflows with no activities |
| MDL003 | Domain model size – Warns when a module has too many entities |
| MDL004 | Validation feedback – Checks for proper validation feedback usage |
| MDL005 | Image source – Validates image widget source configuration |
| MDL006 | Empty containers – Detects container widgets with no children |
| MDL007 | Page navigation security – Checks that pages called from microflows have appropriate access rules |
Security Rules
| Rule | Description |
|---|---|
| SEC001 | Entity access rules – Ensures persistent entities have access rules defined |
| SEC002 | Password policy – Checks for secure password configuration |
| SEC003 | Demo users – Warns about demo users in production security level |
Convention Rules
| Rule | Description |
|---|---|
| CONV011 | No commit in loop – Detects COMMIT statements inside LOOP blocks (performance anti-pattern) |
| CONV012 | Exclusive split captions – Checks that decision branches have meaningful captions |
| CONV013 | Error handling on external calls – Ensures external service calls have error handling |
| CONV014 | No continue error handling – Warns against using CONTINUE error handling without logging |
Running Built-in Rules
Built-in rules run automatically with mxcli lint:
# Run all rules (built-in + Starlark)
mxcli lint -p app.mpr
# List all rules to see which are built-in
mxcli lint -p app.mpr --list-rules
Excluding Modules
System and marketplace modules often trigger false positives. Exclude them:
mxcli lint -p app.mpr --exclude System --exclude Administration
Starlark Rules
In addition to the built-in Go rules, mxcli bundles 27 Starlark-based lint rules. Starlark is a Python-like language that allows rules to be extended and customized without recompiling mxcli.
Bundled Starlark Rules
Security Rules (SEC004-SEC009)
| Rule | Description |
|---|---|
| SEC004 | Guest access – Warns about overly permissive guest/anonymous access |
| SEC005 | Strict mode – Checks for strict security mode settings |
| SEC006 | PII exposure – Detects potentially sensitive data without restricted access |
| SEC007 | Anonymous access – Flags entities accessible to anonymous users |
| SEC008 | Member restrictions – Checks for overly broad member-level access |
| SEC009 | Additional security rules |
Architecture Rules (ARCH001-ARCH003)
| Rule | Description |
|---|---|
| ARCH001 | Cross-module data – Detects tight coupling between modules via direct data access |
| ARCH002 | Microflow-based writes – Ensures data modifications go through microflows |
| ARCH003 | Entity business keys – Checks that entities have meaningful business key attributes |
Quality Rules (QUAL001-QUAL004)
| Rule | Description |
|---|---|
| QUAL001 | McCabe complexity – Flags microflows with high cyclomatic complexity |
| QUAL002 | Documentation – Checks for missing documentation on public elements |
| QUAL003 | Long microflows – Warns about microflows with too many activities |
| QUAL004 | Orphaned elements – Detects unused entities, microflows, or pages |
Design Rules (DESIGN001)
| Rule | Description |
|---|---|
| DESIGN001 | Entity attribute count – Warns when entities have too many attributes |
Convention Rules (CONV001-CONV010, CONV015-CONV017)
| Rule | Description |
|---|---|
| CONV001-CONV010 | Best practice conventions including boolean naming, page suffixes, enumeration prefixes, snippet prefixes |
| CONV015 | Validation rules – Checks for consistent validation patterns |
| CONV016 | Event handlers – Validates event handler configuration |
| CONV017 | Calculated attributes – Checks calculated attribute patterns |
Additional convention rules cover access rule constraints, role mapping, microflow size and content.
Where Starlark Rules Live
When you run mxcli init, Starlark rules are installed to:
your-project/
└── .claude/
└── lint-rules/
├── sec004_guest_access.star
├── arch001_cross_module.star
├── qual001_complexity.star
└── ...
Running Starlark Rules
Starlark rules run automatically alongside built-in rules:
mxcli lint -p app.mpr
Use --list-rules to see all available rules:
mxcli lint -p app.mpr --list-rules
Writing Custom Rules
You can extend the linting framework by writing custom rules in Starlark, a Python-like language. Custom rules are placed in the .claude/lint-rules/ directory and are automatically picked up by mxcli lint.
Rule File Structure
Each Starlark rule file defines a rule with metadata and a check function. Place your rule files in .claude/lint-rules/ with a .star extension.
Getting Started
- Create a new
.starfile in.claude/lint-rules/ - Define the rule metadata (ID, name, description, severity)
- Implement the check function that inspects project elements
- Run
mxcli lint -p app.mprto test your rule
Example: Custom Naming Rule
# .claude/lint-rules/custom001_entity_prefix.star
rule_id = "CUSTOM001"
rule_name = "Entity Prefix Convention"
rule_description = "All entities should be prefixed with their module abbreviation"
severity = "warning"
def check(context):
findings = []
for entity in context.entities:
# Check if entity name starts with module prefix
module_prefix = entity.module_name[:3]
if not entity.name.startswith(module_prefix):
findings.append({
"message": "Entity '%s' should start with module prefix '%s'" % (entity.name, module_prefix),
"element": entity.qualified_name,
})
return findings
Rule Severity Levels
| Severity | Description |
|---|---|
error | Must be fixed; blocks CI pipelines |
warning | Should be fixed; potential issue |
info | Informational; suggestion for improvement |
Testing Custom Rules
# Run lint to test your custom rule
mxcli lint -p app.mpr
# List rules to verify your rule is detected
mxcli lint -p app.mpr --list-rules
Best Practices
- Use a unique rule ID prefix (e.g.,
CUSTOM001) to avoid conflicts with built-in rules - Include clear, actionable messages that explain what to fix
- Test rules against a real project before deploying
- Keep rules focused on a single concern
mxcli lint
The mxcli lint command runs all lint rules (built-in Go rules and Starlark rules) against a Mendix project and reports findings.
Basic Usage
# Lint a project
mxcli lint -p app.mpr
# With colored terminal output
mxcli lint -p app.mpr --color
Output Formats
Text (Default)
Human-readable output with rule IDs, severity, and messages:
mxcli lint -p app.mpr
JSON
Machine-readable JSON output for CI integration:
mxcli lint -p app.mpr --format json
SARIF
SARIF (Static Analysis Results Interchange Format) output for GitHub Code Scanning and other SARIF-compatible tools:
mxcli lint -p app.mpr --format sarif > results.sarif
This file can be uploaded to GitHub as a code scanning result.
Options
List Available Rules
mxcli lint -p app.mpr --list-rules
Shows all available rules with their IDs, names, severity, and source (built-in or Starlark).
Exclude Modules
System and marketplace modules often trigger false positives. Exclude them:
mxcli lint -p app.mpr --exclude System --exclude Administration
Colored Output
mxcli lint -p app.mpr --color
CI Integration
GitHub Actions
- name: Lint Mendix project
run: |
mxcli lint -p app.mpr --format sarif > results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
JSON Processing
# Count findings by severity
mxcli lint -p app.mpr --format json | jq 'group_by(.severity) | map({severity: .[0].severity, count: length})'
# Filter for errors only
mxcli lint -p app.mpr --format json | jq '[.[] | select(.severity == "error")]'
Exit Codes
| Code | Meaning |
|---|---|
| 0 | No findings |
| 1 | Findings reported |
| 2 | Error running linter |
mxcli report
The mxcli report command generates a scored best practices report for a Mendix project. It runs all lint rules and aggregates findings into a category-based scorecard.
Basic Usage
# Generate a report (text output to terminal)
mxcli report -p app.mpr
Output Formats
Markdown
mxcli report -p app.mpr --format markdown
mxcli report -p app.mpr --format markdown --output report.md
HTML
Visual report with color-coded scoring:
mxcli report -p app.mpr --format html --output report.html
JSON
Machine-readable report for CI pipelines:
mxcli report -p app.mpr --format json
Scoring Categories
The report scores the project across 6 categories on a 0-100 scale:
| Category | What It Measures |
|---|---|
| Security | Access rules, password policy, demo users, PII exposure |
| Quality | Documentation coverage, complexity, orphaned elements |
| Architecture | Module coupling, data access patterns, business keys |
| Performance | Commit-in-loop, query patterns |
| Naming | Entity, attribute, microflow, and page naming conventions |
| Design | Entity size, attribute counts, association patterns |
Each category shows:
- A score from 0 to 100
- Number of findings in that category
- Specific rule violations with affected elements
Writing Reports to Files
# Write HTML report
mxcli report -p app.mpr --format html --output report.html
# Write Markdown report
mxcli report -p app.mpr --format markdown --output report.md
# Write JSON report
mxcli report -p app.mpr --format json --output report.json
CI Integration
Use the JSON format to fail CI pipelines when scores drop below a threshold:
# Check if overall score is above 70
SCORE=$(mxcli report -p app.mpr --format json | jq '.overallScore')
if [ "$SCORE" -lt 70 ]; then
echo "Quality score $SCORE is below threshold 70"
exit 1
fi
Terminal UI (TUI)
mxcli includes a terminal-based interactive UI for browsing, inspecting, and modifying Mendix projects. Built with Bubble Tea, it provides a ranger/yazi-style Miller column interface directly in the terminal.
Quick Start
# Launch with a project file
mxcli tui -p app.mpr
# Resume a previous session
mxcli tui -c
# Launch without a project (opens file picker)
mxcli tui
The TUI opens in full-screen alternate screen mode with mouse support enabled.
Feature Summary
| Feature | Description |
|---|---|
| Miller column navigation | Three-panel layout: modules, documents, preview |
| Vim-style keybindings | h/j/k/l navigation, / to filter, : for commands |
| Syntax highlighting | Real-time MDL, SQL, and NDSL highlighting via Chroma |
| Command palette | VS Code-style : command bar with tab completion |
| MDL execution | Type or paste MDL scripts and execute them in-place |
| mx check integration | Validate projects with grouped errors, navigation, and filtering |
| Mouse support | Click to select, scroll wheel, clickable breadcrumbs |
| Session restore | -c flag restores previous tabs, selections, and navigation state |
| File watcher | Auto-detects MPR changes and refreshes the tree |
| Fullscreen preview | Press Enter or Z to expand any panel to full screen |
| Tab support | Multiple project tabs in a single session |
| Contextual help | Press ? for keybinding reference |
Architecture
The TUI delegates all heavy lifting to existing mxcli logic. The project tree is built by calling buildProjectTree() directly, while commands like describe, callers, check, and exec run mxcli as a subprocess. Diagrams open in the system browser via xdg-open or open.
Flags
| Flag | Description |
|---|---|
-p, --project | Path to the .mpr project file |
-c, --continue | Restore the previous TUI session (tabs, navigation, preview mode) |
When launched without -p, the TUI presents a file picker that shows recently opened projects and allows browsing the filesystem for .mpr files.
Keyboard Reference
| Key | Action |
|---|---|
h / Left | Move focus left |
l / Right / Enter | Move focus right / open |
j / Down | Move down |
k / Up | Move up |
Tab | Cycle panel focus |
/ | Filter current column |
: | Open command palette |
x | Open MDL execution view |
z / Z | Toggle fullscreen zoom on current panel |
? | Show contextual help |
q | Quit |
Navigation and Layout
The TUI uses a Miller column layout for hierarchical browsing of Mendix projects. This design, inspired by file managers like ranger and yazi, shows the current context alongside parent and child items for spatial orientation.
Three-Column Layout
The default layout divides the terminal into three panels:
| Panel | Content | Width |
|---|---|---|
| Left | Modules and folders | ~20% |
| Center | Documents within the selected module (entities, microflows, pages, etc.) | ~30% |
| Right | Preview of the selected document (MDL/NDSL rendering with syntax highlighting) | ~50% |
The layout adapts dynamically as you navigate:
- One panel: Only the module list is visible when no module is selected.
- Two panels: Selecting a module expands the document list alongside it (35% + 65% split).
- Three panels: Selecting a document adds the preview panel (20% + 30% + 50% split).
Breadcrumb Navigation
Each panel displays a breadcrumb trail at the top showing the current navigation path. For example:
MyModule > DomainModel > Customer
Breadcrumbs are clickable with the mouse. Clicking a segment navigates back to that level in the hierarchy. The navigation stack remembers your path so you can drill into nested folders and return.
Keyboard Navigation
The TUI uses vim-style keybindings for movement:
| Key | Action |
|---|---|
j / Down | Move cursor down in the current panel |
k / Up | Move cursor up in the current panel |
h / Left | Move focus to the left panel (or go back in navigation stack) |
l / Right / Enter | Move focus to the right panel (or drill into the selected item) |
Tab | Cycle focus between panels (left, center, right) |
/ | Activate filter mode in the current panel |
Esc | Exit filter mode, close overlay, or cancel current action |
Filtering
Press / to activate the filter input in the current panel. Type a substring to filter the list in real time. Only items matching the filter text are shown. Press Esc to clear the filter and return to the full list.
Mouse Support
The TUI enables mouse interaction for all panels:
| Action | Effect |
|---|---|
| Click on an item | Selects it and focuses the panel |
| Scroll wheel | Scrolls the list up or down |
| Click on a breadcrumb segment | Navigates back to that level |
Mouse coordinates are translated to panel-local positions using the layout geometry, so clicks always target the correct panel regardless of terminal size.
Fullscreen Overlay
Press Enter or Z on a selected document to open a fullscreen overlay showing the complete document content. The overlay includes:
- A title bar with the document name and type
- Scrollable content with syntax highlighting (MDL, SQL, or NDSL)
- Hint bar at the bottom showing available keys
Press Esc or q to close the overlay and return to the three-column browser.
Zoom Mode
Press z to zoom the currently focused panel to fill the entire terminal. This is useful for reviewing long module lists or document trees. Press z again or Esc to return to the previous layout. The TUI remembers which panel was focused and which layout was active before zooming.
Tab Support
The TUI supports multiple project tabs within a single session. Each tab maintains its own:
- Project file path
- Navigation state (selected module, document, preview)
- Panel focus and scroll positions
Tabs allow you to compare or cross-reference elements across different projects or different areas of the same project.
Contextual Help
Press ? at any time to open the help overlay. The help content is contextual – it shows keybindings relevant to the current mode (browser, overlay, command palette, or execution view).
Command Palette
Press : to open the command palette at the bottom of the TUI. The command bar provides VS Code-style command execution with tab completion and fuzzy matching.
Usage
- Press
:to activate the command bar. - Start typing a command name. Tab completion suggests matching commands.
- Press
Enterto execute orEscto cancel.
Available Commands
| Command | Description |
|---|---|
:check | Validate the project with mx check |
:run | Execute the current MDL file |
:callers | Show callers of the selected element |
:callees | Show callees of the selected element |
:context | Show context of the selected element |
:impact | Show impact analysis of the selected element |
:refs | Show references to the selected element |
:diagram | Open a diagram of the selected element in the system browser |
:search <keyword> | Perform full-text search across the project |
Tab Completion
The command bar supports multi-level tab completion:
- Level 1 – Command name: Type the first few characters and press
Tabto complete. For example,cal+Tabcompletes tocallers. - Level 2 – Subcommand: Some commands have subcommands. Tab cycles through available options.
- Level 3 – Qualified name: Commands that operate on elements (callers, callees, refs, impact, context) complete with qualified names from the project tree. For example,
:callers MyMod+Tabexpands to:callers MyModule.ACT_CreateCustomer.
Fuzzy matching allows partial input. Typing :cal matches :callers and :callees.
Code Navigation Commands
The :callers, :callees, :refs, :impact, and :context commands display their results in a fullscreen overlay with syntax highlighting. These commands require a prior REFRESH CATALOG FULL to populate the cross-reference index.
When the currently selected element in the browser is a microflow, page, or other navigable document, these commands automatically use it as the target. You can also provide an explicit qualified name:
:callers MyModule.ACT_CreateCustomer
:impact MyModule.Customer
Search
The :search command takes a keyword argument and performs full-text search across all project strings:
:search validation
:search Customer
Results appear in a fullscreen overlay listing matching elements with their type and location.
Diagram
The :diagram command opens a visual diagram of the selected element in the system browser. It uses xdg-open on Linux or open on macOS to launch the default browser. This is useful for viewing domain model relationships or microflow graphs in a visual format.
MDL Execution
The TUI includes a built-in MDL execution view for running MDL statements directly from the terminal interface. Press x to open the execution view.
Opening the Execution View
From the browser mode, press x to switch to the MDL execution view. A full-screen textarea appears with line numbers and a placeholder prompt.
Keybindings
| Key | Action |
|---|---|
x | Open the execution view (from browser mode) |
Ctrl+E | Execute the MDL script in the textarea |
Ctrl+O | Open a file picker to load an MDL file |
Esc | Close the execution view and return to the browser |
Writing and Executing MDL
The textarea supports standard text editing. Type or paste any MDL statements:
SHOW MODULES;
CREATE ENTITY MyModule.Customer
NAME "Customer"
PERSISTENT
ATTRIBUTE Name STRING(100)
ATTRIBUTE Email STRING(200)
ATTRIBUTE Age INTEGER;
Press Ctrl+E to execute. The TUI writes the content to a temporary file and runs mxcli exec as a subprocess. During execution, a status indicator shows “Executing…” in the status bar.
Viewing Results
After execution completes, results appear in a fullscreen overlay with syntax highlighting. The overlay shows:
- The command output for successful executions (e.g., confirmation messages, query results)
- Error details for failed executions, prefixed with
-- Error:
Press q or Esc to close the result overlay.
Tree Refresh
When an MDL script executes successfully (e.g., creating or modifying elements), the project tree automatically refreshes to reflect the changes. This means you can create an entity, close the result overlay, and immediately see it in the browser.
Loading from File
Press Ctrl+O to open a file picker dialog. The TUI attempts to use a native file picker:
- Linux:
zenity --file-selectionorkdialog --getopenfilename - macOS: Native file dialog
The file picker filters for .mdl files. After selecting a file, its content is loaded into the textarea. A status message confirms the loaded file path. You can then review or edit the content before executing with Ctrl+E.
If no file picker is available, the TUI displays an informational message suggesting installation of zenity or kdialog.
Supported MDL Statements
The execution view accepts any valid MDL statement, including:
| Category | Examples |
|---|---|
| Query | SHOW MODULES, SHOW ENTITIES, DESCRIBE ENTITY MyModule.Customer |
| Create | CREATE ENTITY, CREATE MICROFLOW, CREATE PAGE |
| Modify | ALTER ENTITY, ALTER PAGE, ALTER NAVIGATION |
| Delete | DROP ENTITY, DROP MICROFLOW, DROP PAGE |
| Security | GRANT, REVOKE, CREATE USER ROLE |
| Catalog | REFRESH CATALOG, SELECT FROM CATALOG.ENTITIES |
| Search | SEARCH 'keyword' |
Multiple statements can be included in a single execution, separated by semicolons.
Project Validation
The TUI integrates with mx check to validate Mendix projects directly from the terminal interface. Errors, warnings, and deprecations are displayed in an interactive overlay with grouping, navigation, and filtering.
Running a Check
Use the :check command from the command palette (press : then type check) to validate the current project. The TUI runs mx check -j -w -d in the background and displays results in a fullscreen overlay.
Error Overlay
The check results overlay groups diagnostics by error code and deduplicates repeated occurrences:
mx check Results [All: 8E 2W 1D]
CE1613 -- The selected association/attribute no longer exists
MyFirstModule.P_ComboBox_Enum (Page)
> Property 'Association' of combo box 'cmbPriority'
MyFirstModule.P_ComboBox_Assoc (Page) (x7)
> Property 'Attribute' of combo box 'cmbCategory'
Each group shows:
- The error code and description
- Affected documents with their type
- Specific element details
- Occurrence count for deduplicated entries (shown as
(xN))
Filtering by Severity
The overlay supports filtering by diagnostic severity. Press Tab to cycle through filters:
| Filter | Shows |
|---|---|
| All | Errors, warnings, and deprecations |
| Errors | Only errors |
| Warnings | Only warnings |
| Deprecations | Only deprecations |
The title bar reflects the active filter: [All: 8E 2W 1D] or [Errors: 8].
Error Navigation
The check overlay is a selectable list. Use j/k to move the cursor between error locations. Press Enter on a location to:
- Close the overlay
- Navigate the project tree to the affected document
After closing the overlay, the TUI enters check navigation mode. The status bar shows the current error position and navigation hints:
[2/5] CE1613: MyFirstModule.P_ComboBox_Enum ]e next [e prev
Navigation Keys
| Key | Action |
|---|---|
]e | Jump to the next error location in the project tree |
[e | Jump to the previous error location |
! | Reopen the check overlay |
Esc | Exit check navigation mode |
File Monitoring
The TUI watches the project file for changes. When it detects that the MPR file has been modified (e.g., after saving in Studio Pro or executing an MDL script), it can automatically re-run mx check and update the results. This provides a continuous validation feedback loop when making changes.
LLM Anchor Tags
The check overlay embeds faint structured anchor tags in its output for machine consumption. These tags are nearly invisible in the terminal but are preserved when copying text or taking screenshots:
[mxcli:check] errors=8 warnings=2 deprecations=1
[mxcli:check:CE1613] severity=ERROR count=6 doc=MyFirstModule.P_ComboBox_Assoc type=Page
These anchors enable AI assistants to parse check results from terminal screenshots or clipboard content, providing structured data for automated error analysis and fix suggestions.
Status Badge
When check results are available, a compact badge appears in the TUI status bar showing the diagnostic summary:
8E 2W 1D
This badge updates whenever a new check completes, providing an at-a-glance project health indicator without opening the full overlay.
Session Management
The TUI supports session persistence, allowing you to restore your previous browsing state when relaunching the tool.
Restoring a Session
Use the -c or --continue flag to restore the previous session:
mxcli tui -c
This restores:
- Open project tabs and their file paths
- Navigation state (selected module, document, scroll positions)
- Panel focus and layout configuration
- Preview mode settings
If the session file cannot be loaded (e.g., first launch or corrupted state), a warning is printed to stderr and the TUI starts fresh.
Combining with Project Flag
When using -c with -p, the explicit project path takes precedence:
# Restore session but open a specific project
mxcli tui -c -p app.mpr
When using -c alone, the project path is loaded from the saved session. If the session contains multiple tabs, the first tab’s project path is used.
Session Storage
Session state is stored locally in the user’s configuration directory. The TUI automatically saves session state when exiting normally (via q).
Project File Picker
When launching without either -p or -c, the TUI presents an interactive file picker:
mxcli tui
The picker shows:
- Recently opened projects (from session history)
- Filesystem browser filtered to
.mprfiles
Select a project and press Enter to launch the TUI. The selected project is saved to the history for future sessions.
Session History
The TUI maintains a history of opened projects. This history is separate from the full session state and persists across sessions. It powers:
- The file picker’s “recent projects” list
- Session restore when using
-c
Each time a project is opened, it is added to the history via tui.SaveHistory(projectPath).
Testing
mxcli includes a testing framework for validating Mendix projects using MDL test files. Tests verify that MDL scripts execute correctly and that the resulting project passes validation.
Overview
The testing framework supports two test file formats:
| Format | Extension | Description |
|---|---|---|
| MDL test files | .test.mdl | Pure MDL scripts with test annotations |
| Markdown test files | .test.md | Literate tests with prose and embedded MDL code blocks |
Prerequisites
Running tests requires Docker for Mendix runtime validation. The test runner:
- Creates a fresh Mendix project using
mx create-project - Executes the MDL test script against the project
- Validates the result with
mx check - Reports pass/fail results
Quick Start
# Run all tests in a directory
mxcli test tests/ -p app.mpr
# Run a specific test file
mxcli test tests/sales.test.mdl -p app.mpr
Test Workflow
- Write test files using
.test.mdlor.test.mdformat - Add
@testand@expectannotations for assertions - Run tests with
mxcli test - Review results
Example Test
-- tests/customer.test.mdl
-- @test Create customer entity
CREATE PERSISTENT ENTITY MyFirstModule.Customer (
Name: String(200) NOT NULL,
Email: String(200)
);
-- @expect 0 errors
-- @test Add association
CREATE ASSOCIATION MyFirstModule.Order_Customer
FROM MyFirstModule.Order
TO MyFirstModule.Customer
TYPE Reference;
-- @expect 0 errors
Playwright UI Testing
For browser-based testing that verifies widgets render correctly in the DOM, see Playwright Testing. While mxcli test validates that MDL scripts execute correctly and the project passes mx check, Playwright testing goes further by verifying the running application in a real browser – checking that widgets are visible, forms accept input, and navigation works.
# MDL validation (this page)
mxcli test tests/ -p app.mpr
# Browser-based UI verification
mxcli playwright verify tests/ -p app.mpr
Related Pages
- Test Formats –
.test.mdland.test.mdfile formats - Test Annotations –
@testand@expectannotations - Running Tests –
mxcli testcommand and Docker requirements - Playwright Testing – Browser-based UI testing with playwright-cli
- Diff – Comparing scripts against project state
Test Formats
mxcli supports two test file formats: .test.mdl for pure MDL tests and .test.md for literate tests with documentation.
.test.mdl Format
Pure MDL scripts with test annotations. Each test case is marked with @test and optionally @expect.
-- @test Create a new module
CREATE MODULE TestModule;
-- @expect 0 errors
-- @test Create entity with attributes
CREATE PERSISTENT ENTITY TestModule.Customer (
Name: String(200) NOT NULL,
Email: String(200) UNIQUE,
IsActive: Boolean DEFAULT true
);
-- @expect 0 errors
-- @test Create enumeration
CREATE ENUMERATION TestModule.Status (
Active 'Active',
Inactive 'Inactive'
);
-- @expect 0 errors
-- @test Create microflow
CREATE MICROFLOW TestModule.ACT_Activate(
$customer: TestModule.Customer
) RETURNS Boolean AS $result
BEGIN
DECLARE $result Boolean = false;
CHANGE $customer (IsActive = true);
COMMIT $customer;
SET $result = true;
RETURN $result;
END;
-- @expect 0 errors
.test.md Format
Literate test files that combine prose documentation with embedded MDL code blocks. The test runner extracts and executes the MDL code blocks while ignoring the prose.
# Customer Module Tests
This test suite validates the customer management module.
## Entity Creation
Create the customer entity with standard fields:
```sql
CREATE PERSISTENT ENTITY TestModule.Customer (
Name: String(200) NOT NULL,
Email: String(200)
);
```
## Association Setup
Link customers to their orders:
```sql
CREATE ASSOCIATION TestModule.Order_Customer
FROM TestModule.Order
TO TestModule.Customer
TYPE Reference;
```
The .test.md format is useful for:
- Documenting test intent alongside the test code
- Creating test suites that serve as tutorials
- Sharing test cases with non-technical stakeholders
File Organization
tests/
├── domain-model.test.mdl # Entity and association tests
├── microflows.test.mdl # Microflow logic tests
├── security.test.mdl # Access rule tests
├── pages.test.mdl # Page creation tests
└── integration.test.md # Full integration test with docs
Naming Conventions
- Use descriptive file names that indicate what is being tested
- Group related tests in the same file
- Use the
@testannotation to name individual test cases within a file
Test Annotations
Test annotations control test execution and define expectations. They are placed in comments within .test.mdl files.
@test
Marks the start of a named test case.
Syntax:
-- @test <description>
Example:
-- @test Create customer entity
CREATE PERSISTENT ENTITY MyModule.Customer (
Name: String(200) NOT NULL
);
Each @test annotation starts a new test case. All MDL statements between one @test and the next (or end of file) belong to that test case.
@expect
Defines the expected outcome of a test case.
Syntax:
-- @expect <expectation>
Expected Outcomes
| Expectation | Description |
|---|---|
0 errors | The test should complete with no errors from mx check |
error | The test is expected to produce an error |
<n> errors | The test should produce exactly N errors |
Examples:
-- @test Valid entity creation
CREATE PERSISTENT ENTITY MyModule.Customer (
Name: String(200) NOT NULL
);
-- @expect 0 errors
-- @test Missing module should fail
CREATE PERSISTENT ENTITY NonExistent.Entity (
Name: String(200)
);
-- @expect error
Complete Example
-- @test Setup: Create module and enumeration
CREATE MODULE TestSales;
CREATE ENUMERATION TestSales.OrderStatus (
Draft 'Draft',
Submitted 'Submitted',
Completed 'Completed'
);
-- @expect 0 errors
-- @test Create entity with enum attribute
CREATE PERSISTENT ENTITY TestSales.Order (
OrderNumber: String(50) NOT NULL,
Status: Enumeration(TestSales.OrderStatus) DEFAULT 'Draft',
TotalAmount: Decimal DEFAULT 0
);
-- @expect 0 errors
-- @test Create microflow with validation
CREATE MICROFLOW TestSales.ACT_SubmitOrder(
$order: TestSales.Order
) RETURNS Boolean AS $success
BEGIN
DECLARE $success Boolean = false;
IF $order/TotalAmount <= 0 THEN
VALIDATION FEEDBACK $order/TotalAmount MESSAGE 'Total must be positive';
RETURN false;
END IF;
CHANGE $order (Status = 'Submitted');
COMMIT $order;
SET $success = true;
RETURN $success;
END;
-- @expect 0 errors
-- @test Security: grant access
CREATE MODULE ROLE TestSales.User;
GRANT EXECUTE ON MICROFLOW TestSales.ACT_SubmitOrder TO TestSales.User;
GRANT TestSales.User ON TestSales.Order (CREATE, DELETE, READ *, WRITE *);
-- @expect 0 errors
Notes
- Test cases run sequentially; earlier test cases set up state for later ones
- The test runner validates using
mx checkafter executing each test case (or the full script) - Tests that modify the project are run against an isolated copy, not the original MPR file
Running Tests
The mxcli test command executes test files and reports results.
Prerequisites
Running tests requires Docker for Mendix runtime validation. The test runner uses:
mx create-projectto create a fresh blank Mendix projectmx checkto validate the project after applying MDL changes
The mx binary is located at:
| Environment | Path |
|---|---|
| Dev container | ~/.mxcli/mxbuild/{version}/modeler/mx |
| Repository | reference/mxbuild/modeler/mx |
To auto-download mxbuild for the project’s Mendix version:
mxcli setup mxbuild -p app.mpr
Basic Usage
# Run all tests in a directory
mxcli test tests/ -p app.mpr
# Run a specific test file
mxcli test tests/sales.test.mdl -p app.mpr
# Run markdown test files
mxcli test tests/integration.test.md -p app.mpr
Test Execution Flow
- Create project – A fresh Mendix project is created in a temporary directory using
mx create-project - Execute MDL – The test script is executed against the fresh project using
mxcli exec - Validate –
mx checkvalidates the resulting project for errors - Report – Results are reported with pass/fail status per test case
Isolated Testing Pattern
For manual testing or debugging, you can replicate the test runner’s workflow:
# Create a fresh project
cd /tmp/test-workspace
~/.mxcli/mxbuild/*/modeler/mx create-project
# Apply MDL changes
mxcli exec script.mdl -p /tmp/test-workspace/App.mpr
# Validate
~/.mxcli/mxbuild/*/modeler/mx check /tmp/test-workspace/App.mpr
Expected output for a passing test:
Checking app for errors...
The app contains: 0 errors.
Test Validation Checklist
Before marking tests as complete:
- MDL script executes without errors
mx checkpasses with 0 errors- Created elements appear correctly if inspected with
DESCRIBE - Security rules are valid (no CE0066 errors)
Debugging Test Failures
TypeCacheUnknownTypeException
The type cache does not contain a type with qualified name DomainModels$Index
This means a BSON $Type field uses the wrong name. Check the reflection data for the correct storageName.
CE0066 “Entity access is out of date”
Security access rules don’t match the entity’s current structure. Make sure:
- All attributes in the entity are included in access rules
- Association member access is only on the FROM entity
- Run
GRANTafter adding new attributes
Common Fixes
# Re-run with verbose output
mxcli exec script.mdl -p app.mpr --verbose
# Inspect the generated BSON
mxcli dump-bson -p app.mpr --doc "Module.EntityName"
Playwright Testing
mxcli integrates with playwright-cli for automated UI testing of Mendix applications. This enables verification that generated pages render correctly, widgets appear in the DOM, and navigation works as expected.
Why Browser Testing?
Mendix apps are React single-page applications. The server returns a JavaScript shell, and the actual UI is rendered client-side. An HTTP 200 from /p/Customer_Overview tells you nothing about whether the page’s widgets actually rendered. A button defined in MDL might be missing from the DOM due to conditional visibility, incorrect container nesting, or a BSON serialization issue – none of which are detectable without executing JavaScript in a real browser.
Prerequisites
- Node.js – Required for playwright-cli
- playwright-cli – Install globally:
npm install -g @playwright/cli@latest - Chromium – Install via:
playwright-cli install --with-deps chromium - Running Mendix app – Start with
mxcli docker run -p app.mpr --wait
Devcontainer Setup
If using a devcontainer, add the following to postCreateCommand in your devcontainer.json:
npm install -g @playwright/cli@latest && playwright-cli install --with-deps chromium
The mxcli init command generates a .playwright/cli.config.json with sensible defaults for headless Chromium and a PLAYWRIGHT_CLI_SESSION environment variable for session management.
mxcli playwright verify
The mxcli playwright verify command runs .test.sh scripts against a running Mendix application and collects results.
# Run all test scripts in a directory
mxcli playwright verify tests/ -p app.mpr
# Run a specific script
mxcli playwright verify tests/verify-customers.test.sh
# List discovered scripts without executing
mxcli playwright verify tests/ --list
# Output JUnit XML for CI integration
mxcli playwright verify tests/ -p app.mpr --junit results.xml
# Verbose output (show script stdout/stderr)
mxcli playwright verify tests/ -p app.mpr --verbose
# Custom app URL (auto-detected from .docker/.env by default)
mxcli playwright verify tests/ --base-url http://localhost:9090
# Skip the app health check
mxcli playwright verify tests/ --skip-health-check
Options
| Flag | Default | Description |
|---|---|---|
--list, -l | false | List test scripts without executing |
--junit, -j | Write JUnit XML results to file | |
--verbose, -v | false | Show script stdout/stderr during execution |
--color | false | Use colored terminal output |
--timeout, -t | 2m | Timeout per script execution |
--base-url | http://localhost:8080 | Mendix app base URL |
--skip-health-check | false | Skip app reachability check before running |
-p | Path to the .mpr project file |
How It Works
The verify runner performs these steps:
- Discovers all
.test.shfiles in the provided paths - Checks that
playwright-cliis available in PATH - Health-checks the app at the base URL (unless
--skip-health-check) - Opens a playwright-cli browser session (Chromium by default)
- Runs each
.test.shscript sequentially viabash - Captures a screenshot on failure for debugging
- Closes the browser session
- Reports pass/fail per script with timing
- Writes JUnit XML if
--junitis specified - Exits with non-zero status if any script failed
Test Script Format
Test scripts are plain bash files using playwright-cli commands. The naming convention is tests/verify-<name>.test.sh. Scripts should use set -euo pipefail so that any failing command causes the script to exit with a non-zero code.
Example Test Script
#!/usr/bin/env bash
# tests/verify-customers.test.sh -- Customer module smoke test
set -euo pipefail
# --- Login ---
playwright-cli open http://localhost:8080
playwright-cli fill e12 "MxAdmin"
playwright-cli fill e15 "AdminPassword1!"
playwright-cli click e18
playwright-cli state-save mendix-auth
# --- Customer Overview page ---
playwright-cli goto http://localhost:8080/p/Customer_Overview
playwright-cli run-code "document.querySelector('.mx-name-dgCustomers') !== null"
playwright-cli run-code "document.querySelector('.mx-name-btnNew') !== null"
playwright-cli run-code "document.querySelector('.mx-name-btnEdit') !== null"
# --- Create a customer ---
playwright-cli click btnNew
playwright-cli snapshot
playwright-cli fill txtName "CI Test Customer"
playwright-cli fill txtEmail "ci@test.com"
playwright-cli click btnSave
# --- Verify persistence (via OQL, not direct DB query) ---
mxcli oql -p app.mpr --json \
"SELECT Name FROM MyModule.Customer WHERE Name = 'CI Test Customer'" \
| grep -q "CI Test Customer"
# --- Cleanup ---
playwright-cli close
echo "PASS: verify-customers"
Widget Selectors
Mendix renders each widget’s name property as a CSS class on the corresponding DOM element:
<div class="mx-name-submitButton form-group">
These .mx-name-* classes are stable, predictable, and directly derived from MDL widget names. This makes them ideal for test assertions.
Using Selectors in Scripts
The recommended approach for CI scripts is to use run-code with CSS selectors, since dynamic element refs (e12, e15) change between page loads:
# Check widget presence
playwright-cli run-code "document.querySelector('.mx-name-btnSave') !== null"
# Click a widget
playwright-cli run-code "document.querySelector('.mx-name-btnSave').click()"
# Check text content
playwright-cli run-code "document.querySelector('.mx-name-lblTitle').textContent.includes('Customers')"
For assertions that should cause failures, use throw so the script exits non-zero under set -e:
playwright-cli run-code "if (!document.querySelector('.mx-name-btnSave')) throw new Error('btnSave not found')"
Three Test Layers
Layer 1: Smoke Tests
Fast checks that pages are reachable and the app starts without errors. These run first as a gate before heavier tests.
# HTTP reachability
playwright-cli open http://localhost:8080
playwright-cli goto http://localhost:8080/p/Customer_Overview
playwright-cli goto http://localhost:8080/p/Customer_Edit
Layer 2: UI Widget Tests
Verify that every widget generated in MDL is present and interactive in the DOM.
# Widget presence
playwright-cli run-code "document.querySelector('.mx-name-dgCustomers') !== null"
playwright-cli run-code "document.querySelector('.mx-name-btnNew') !== null"
# Form interaction
playwright-cli run-code "document.querySelector('.mx-name-txtName input').value = 'Test'"
playwright-cli run-code "document.querySelector('.mx-name-btnSave').click()"
Layer 3: Data Assertions
Verify that UI interactions persist the correct data. Use mxcli oql for data validation instead of direct database queries.
# Submit a form via the UI, then verify persistence
mxcli oql -p app.mpr --json \
"SELECT Name, Email FROM MyModule.Customer WHERE Name = 'Test'" \
| grep -q "Test"
Example Output
Found 3 test script(s)
Checking app at http://localhost:8080...
App is reachable
Opening browser session (chromium)...
[1/3] verify-login... PASS (1.2s)
[2/3] verify-customers... PASS (3.4s)
[3/3] verify-orders... FAIL (2.1s)
Screenshot saved: verify-orders-failure.png
Error: btnSubmit not found
Playwright Verify Results
============================================================
PASS verify-login (1.2s)
PASS verify-customers (3.4s)
FAIL verify-orders (2.1s)
Error: btnSubmit not found
------------------------------------------------------------
Total: 3 Passed: 2 Failed: 1 Time: 6.7s
Some scripts failed.
CI/CD Integration
For continuous integration, combine mxcli docker run --wait with mxcli playwright verify:
# Build and start the app
mxcli docker run -p app.mpr --wait
# Run all verification scripts
mxcli playwright verify tests/ -p app.mpr --junit results.xml
# JUnit XML output is consumed by CI systems (GitHub Actions, Jenkins, etc.)
Related Pages
- Testing – MDL test framework (
mxcli test) - Docker Run – Building and running the Mendix app in Docker
- Docker Check – Validating projects without building
Diff
mxcli provides two diff commands for comparing MDL scripts against project state and viewing local changes in MPR v2 projects.
mxcli diff
Compares an MDL script against the current project state, showing what would change if the script were executed. This is a dry-run preview.
Usage:
mxcli diff -p app.mpr changes.mdl
This shows:
- Elements that would be created (new entities, microflows, pages)
- Elements that would be modified (changed attributes, altered properties)
- Elements that would be removed (DROP statements)
Use mxcli diff to review changes before applying them, especially when working with AI-generated scripts.
mxcli diff-local
Compares local changes against a git reference for MPR v2 projects. MPR v2 (Mendix >= 10.18) stores documents as individual files in an mprcontents/ folder, making git diff feasible.
Usage:
# Compare against HEAD (latest commit)
mxcli diff-local -p app.mpr --ref HEAD
# Compare against a specific commit
mxcli diff-local -p app.mpr --ref HEAD~1
# Compare against a branch
mxcli diff-local -p app.mpr --ref main
MPR v2 Requirement
diff-local only works with MPR v2 format (Mendix >= 10.18), where documents are stored as individual files. MPR v1 projects store everything in a single SQLite database, making file-level git diff impractical.
Workflow
Review Before Applying
# 1. Generate MDL changes
# (AI assistant creates changes.mdl)
# 2. Review what would change
mxcli diff -p app.mpr changes.mdl
# 3. If satisfied, apply
mxcli exec changes.mdl -p app.mpr
Track Changes Over Time
# After making changes, see what changed since last commit
mxcli diff-local -p app.mpr --ref HEAD
# See changes since two commits ago
mxcli diff-local -p app.mpr --ref HEAD~2
External SQL
mxcli can connect to external databases (PostgreSQL, Oracle, SQL Server) for querying, schema exploration, data import, and Database Connector generation. This enables workflows like importing reference data from external systems, exploring database schemas, and auto-generating Mendix integration code.
Supported Databases
| Database | Driver Names | DSN Format |
|---|---|---|
| PostgreSQL | postgres, pg, postgresql | postgres://user:pass@host:5432/dbname |
| Oracle | oracle, ora | oracle://user:pass@host:1521/service |
| SQL Server | sqlserver, mssql | sqlserver://user:pass@host:1433?database=dbname |
Security
Credentials are isolated from session output. The DSN (which contains username and password) is never displayed in session output, logs, or error messages. Only the alias and driver name are shown.
Capabilities
| Feature | Command | Description |
|---|---|---|
| Connect | SQL CONNECT | Establish a named connection |
| Explore | SQL <alias> SHOW TABLES/VIEWS | Browse database schema |
| Describe | SQL <alias> DESCRIBE <table> | View column details |
| Query | SQL <alias> <any-sql> | Run arbitrary SQL |
| Import | IMPORT FROM <alias> | Import data into Mendix app DB |
| Generate | SQL <alias> GENERATE CONNECTOR | Auto-generate Database Connector MDL |
| Disconnect | SQL DISCONNECT <alias> | Close connection |
Quick Start
-- Connect to an external database
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
-- Explore the schema
SQL source SHOW TABLES;
SQL source DESCRIBE employees;
-- Query data
SQL source SELECT * FROM employees WHERE active = true LIMIT 10;
-- Import into Mendix
IMPORT FROM source QUERY 'SELECT name, email FROM employees'
INTO HR.Employee
MAP (name AS Name, email AS Email);
-- Generate Database Connector MDL
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;
-- Disconnect
SQL DISCONNECT source;
CLI Subcommand
For one-off queries without the REPL:
mxcli sql --driver postgres --dsn 'postgres://user:pass@localhost:5432/mydb' "SELECT * FROM users"
Related Pages
- SQL CONNECT – Connection syntax and driver details
- Querying External Databases – Query syntax and schema exploration
- IMPORT FROM – Data import pipeline
- Credential Management – Environment variables and YAML config
- Database Connector Generation – Auto-generating integration code
SQL CONNECT
The SQL CONNECT command establishes a named connection to an external database.
Syntax
SQL CONNECT <driver> '<dsn>' AS <alias>
Parameters:
| Parameter | Description |
|---|---|
<driver> | Database driver name |
<dsn> | Data Source Name (connection string) |
<alias> | Short name to reference this connection |
Supported Drivers
| Database | Driver Names |
|---|---|
| PostgreSQL | postgres, pg, postgresql |
| Oracle | oracle, ora |
| SQL Server | sqlserver, mssql |
Connection Examples
PostgreSQL
SQL CONNECT postgres 'postgres://user:password@localhost:5432/mydb' AS source;
-- With SSL
SQL CONNECT postgres 'postgres://user:password@host:5432/mydb?sslmode=require' AS prod;
Oracle
SQL CONNECT oracle 'oracle://user:password@host:1521/service_name' AS oradb;
SQL Server
SQL CONNECT sqlserver 'sqlserver://user:password@host:1433?database=mydb' AS mssql;
Managing Connections
List Active Connections
SQL CONNECTIONS;
This shows alias and driver name only. The DSN is never displayed for security reasons.
Disconnect
SQL DISCONNECT source;
Multiple Connections
You can maintain multiple simultaneous connections with different aliases:
SQL CONNECT postgres 'postgres://...' AS source_db;
SQL CONNECT oracle 'oracle://...' AS legacy_db;
SQL CONNECT sqlserver 'sqlserver://...' AS reporting_db;
-- Query each by alias
SQL source_db SELECT count(*) FROM users;
SQL legacy_db SELECT count(*) FROM employees;
SQL reporting_db SELECT count(*) FROM reports;
-- List all connections
SQL CONNECTIONS;
-- Disconnect individually
SQL DISCONNECT legacy_db;
CLI Subcommand
For one-off queries without establishing a named connection:
mxcli sql --driver postgres --dsn 'postgres://user:pass@localhost:5432/mydb' "SELECT 1"
Security Notes
- The DSN (which contains credentials) is never shown in session output or logs
- Only the alias and driver type are displayed when listing connections
- See Credential Management for environment variable and YAML config alternatives
Querying External Databases
Once connected to an external database, you can explore its schema and run arbitrary SQL queries.
Schema Exploration
Show Tables
SQL <alias> SHOW TABLES;
Lists all user tables in the connected database.
Show Views
SQL <alias> SHOW VIEWS;
Lists all user views.
Show Functions
SQL <alias> SHOW FUNCTIONS;
Lists functions and stored procedures.
Describe Table
SQL <alias> DESCRIBE <table>;
Shows column details including name, type, and nullability.
Running Queries
Any SQL statement can be passed through to the connected database:
SQL <alias> <any-sql>;
Examples
-- Connect
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
-- Explore schema
SQL source SHOW TABLES;
SQL source DESCRIBE employees;
-- Simple queries
SQL source SELECT * FROM employees WHERE active = true LIMIT 10;
-- Aggregation
SQL source SELECT department, COUNT(*) as cnt FROM employees GROUP BY department ORDER BY cnt DESC;
-- Joins
SQL source SELECT e.name, d.name AS department
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE e.active = true;
-- DDL (if you have permissions)
SQL source CREATE INDEX idx_employees_email ON employees(email);
CLI Subcommand
For one-off queries from the command line:
# Direct query
mxcli sql --driver postgres --dsn 'postgres://user:pass@localhost:5432/mydb' "SELECT * FROM users LIMIT 5"
Interactive Workflow
Within the REPL, use SQL commands interactively:
-- Connect
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
-- Explore
SQL source SHOW TABLES;
SQL source DESCRIBE users;
-- Query
SQL source SELECT id, name, email FROM users WHERE created_at > '2024-01-01' LIMIT 10;
-- Done
SQL DISCONNECT source;
Output
Query results are displayed in a formatted table by default. The output format can be controlled with session variables:
SET output_format = 'json';
SQL source SELECT * FROM users LIMIT 5;
IMPORT FROM
The IMPORT command imports data from an external database into a Mendix application’s PostgreSQL database. It supports column mapping, association linking, batch processing, and row limits.
Syntax
IMPORT FROM <alias> QUERY '<sql>'
INTO <Module.Entity>
MAP (<source-col> AS <AttrName> [, ...])
[LINK (<source-col> TO <AssocName> ON <MatchAttr>) [, ...]]
[BATCH <size>]
[LIMIT <count>]
Parameters
| Parameter | Description |
|---|---|
<alias> | Named connection established with SQL CONNECT |
QUERY '<sql>' | SQL query to execute against the external database |
INTO <Module.Entity> | Target Mendix entity to insert into |
MAP (...) | Column-to-attribute mapping |
LINK (...) | Association linking via lookup |
BATCH <size> | Number of rows per batch insert (default varies) |
LIMIT <count> | Maximum number of rows to import |
Basic Import
Map source columns to entity attributes:
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
IMPORT FROM source QUERY 'SELECT name, email FROM employees'
INTO HR.Employee
MAP (name AS Name, email AS Email);
Import with Association Linking
The LINK clause creates associations by looking up related entities:
IMPORT FROM source QUERY 'SELECT name, dept_name FROM employees'
INTO HR.Employee
MAP (name AS Name)
LINK (dept_name TO Employee_Department ON Name);
This matches the dept_name value from the source query against the Name attribute of the entity on the other side of the Employee_Department association, creating the association link for each imported row.
Batch and Limit Control
IMPORT FROM source QUERY 'SELECT * FROM products'
INTO Catalog.Product
MAP (name AS Name, price AS Price)
BATCH 500
LIMIT 1000;
BATCH 500– Insert 500 rows at a time (reduces memory usage)LIMIT 1000– Stop after importing 1000 rows
How It Works
The IMPORT pipeline:
- Executes the source query against the external database
- Generates Mendix IDs for each new row (proper Mendix UUID format)
- Maps columns to entity attributes based on the MAP clause
- Resolves associations by looking up matching entities for LINK clauses
- Batch inserts into the Mendix application’s PostgreSQL database
- Tracks sequences to ensure auto-number fields continue correctly
Auto-Connection to Mendix DB
The IMPORT command automatically connects to the Mendix application’s PostgreSQL database based on the project’s configuration settings. You do not need to manually connect to the Mendix database.
Complete Example
-- Connect to external source
SQL CONNECT postgres 'postgres://user:pass@legacy-db:5432/hrdb' AS legacy;
-- Preview the data
SQL legacy SELECT name, email, department FROM employees LIMIT 5;
-- Import employees
IMPORT FROM legacy QUERY 'SELECT name, email FROM employees WHERE active = true'
INTO HR.Employee
MAP (name AS FullName, email AS EmailAddress)
BATCH 500;
-- Import with association linking
IMPORT FROM legacy QUERY 'SELECT name, dept_name FROM employees WHERE active = true'
INTO HR.Employee
MAP (name AS FullName)
LINK (dept_name TO Employee_Department ON DepartmentName)
BATCH 500
LIMIT 10000;
-- Disconnect
SQL DISCONNECT legacy;
Notes
- The target entity must already exist in the Mendix project
- Column types are automatically converted where possible
- Association linking requires the target entity to have matching values in the lookup attribute
- The Mendix application database must be a PostgreSQL instance accessible from the mxcli host
Credential Management
mxcli provides secure credential management for external database connections. Credentials are isolated from session output, logs, and error messages.
Security Principles
- DSN isolation – Connection strings (which contain passwords) are never displayed in session output
- Alias-only display –
SQL CONNECTIONSonly shows alias and driver type - Log sanitization – Credentials are stripped from log output and error messages
Connection Methods
Inline DSN
The simplest approach, suitable for development:
SQL CONNECT postgres 'postgres://user:password@localhost:5432/mydb' AS source;
Environment Variables
Store credentials in environment variables for CI/CD and production:
# Set environment variable
export MYDB_DSN='postgres://user:password@host:5432/mydb'
# Use in mxcli (depends on your shell expanding the variable)
SQL CONNECT postgres '$MYDB_DSN' AS source;
YAML Configuration
mxcli supports YAML configuration files for managing multiple database connections. The configuration file stores DSN information that can be referenced by name.
Configuration is resolved from:
- Environment variables
- YAML config files
- Inline DSN strings
CLI Subcommand
The mxcli sql subcommand accepts credentials via command-line flags:
mxcli sql --driver postgres --dsn 'postgres://user:pass@host:5432/db' "SELECT 1"
For CI/CD pipelines, use environment variables:
export DB_DSN='postgres://user:pass@host:5432/db'
mxcli sql --driver postgres --dsn "$DB_DSN" "SELECT * FROM users"
Best Practices
- Never commit credentials to version control
- Use environment variables in CI/CD pipelines
- Use YAML config for local development with multiple databases
- Rotate credentials regularly
- Use read-only database users when only querying (not importing)
Mendix Application Database
For IMPORT commands that write to the Mendix application database, the connection is automatically established using the project’s configuration settings (from DESCRIBE SETTINGS). The Mendix database DSN is built from the project’s DatabaseType, DatabaseUrl, DatabaseName, DatabaseUserName, and DatabasePassword settings.
Database Connector Generation
The SQL GENERATE CONNECTOR command auto-generates Mendix Database Connector MDL from an external database schema. This creates the entities, microflows, and configuration needed to query external databases from within a Mendix application.
Syntax
SQL <alias> GENERATE CONNECTOR INTO <module> [TABLES (<table1>, <table2>, ...)] [VIEWS (<view1>, ...)] [EXEC]
Parameters
| Parameter | Description |
|---|---|
<alias> | Named connection established with SQL CONNECT |
INTO <module> | Target Mendix module to generate the connector in |
TABLES (...) | Optional: specific tables to include (default: all) |
VIEWS (...) | Optional: specific views to include |
EXEC | Execute the generated MDL immediately (otherwise just output it) |
Examples
Generate for All Tables
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
-- Preview the generated MDL (output only, no changes)
SQL source GENERATE CONNECTOR INTO HRModule;
Generate for Specific Tables
-- Generate for selected tables only
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments);
Generate and Execute
-- Generate and immediately apply to the project
SQL source GENERATE CONNECTOR INTO HRModule
TABLES (employees, departments)
EXEC;
The EXEC flag executes the generated MDL against the connected project, creating the entities and microflows in one step.
What Gets Generated
The connector generation inspects the external database schema and produces:
- Non-persistent entities for each table/view, with attributes matching column types
- Microflow activities using
EXECUTE DATABASE QUERYfor querying the external database - SQL type mapping from database column types to Mendix attribute types
Type Mapping
External database types are mapped to Mendix types:
| SQL Type | Mendix Type |
|---|---|
VARCHAR, TEXT, CHAR | String |
INTEGER, INT, SMALLINT | Integer |
BIGINT | Long |
NUMERIC, DECIMAL, FLOAT, DOUBLE | Decimal |
BOOLEAN, BIT | Boolean |
DATE, TIMESTAMP, DATETIME | DateTime |
BYTEA, BLOB | Binary |
Workflow
-- 1. Connect to the external database
SQL CONNECT postgres 'postgres://user:pass@host:5432/db' AS source;
-- 2. Explore the schema
SQL source SHOW TABLES;
SQL source DESCRIBE employees;
-- 3. Preview the generated MDL
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments);
-- 4. Review the output, then execute
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;
-- 5. Disconnect
SQL DISCONNECT source;
Docker Integration
mxcli provides Docker integration for building, running, and validating Mendix applications without a local Mendix installation. Docker is also required for the testing framework.
Features
| Command | Description |
|---|---|
mxcli docker build | Build a Mendix application with mxbuild in Docker, including PAD patching |
mxcli docker run | Run a Mendix application in a Docker container |
mxcli docker check | Validate a project with mx check (auto-downloads mxbuild) |
mxcli test | Run test files (uses Docker for mx create-project and mx check) |
mxcli oql | Query a running Mendix runtime via M2EE admin API |
Prerequisites
- Docker must be installed and running
- The Docker daemon must be accessible from the command line
The mx Tool
The mx command-line tool validates and builds Mendix projects. mxcli can auto-download the correct version:
# Auto-download mxbuild for the project's Mendix version
mxcli setup mxbuild -p app.mpr
The mx binary location depends on the environment:
| Environment | Path |
|---|---|
| Dev container | ~/.mxcli/mxbuild/{version}/modeler/mx |
| Repository | reference/mxbuild/modeler/mx |
Quick Start
# Validate a project
mxcli docker check -p app.mpr
# Build a deployable package
mxcli docker build -p app.mpr
Related Pages
- mxcli docker build – Building with PAD patching
- mxcli docker run – Running applications
- OQL Queries – Querying running applications
- Dev Container Setup – Development environment
mxcli docker build
The mxcli docker build command builds a Mendix application using mxbuild in Docker, including support for PAD (Platform-Agnostic Deployment) patching.
Usage
mxcli docker build -p app.mpr
What It Does
- Downloads mxbuild – Automatically downloads the correct mxbuild version for your project if not already available
- Runs mxbuild – Executes the Mendix build toolchain in a Docker container
- PAD patching – Applies Platform-Agnostic Deployment patches (Phase 1) to the build output
- Produces artifact – Generates a deployable Mendix deployment package (MDA file)
PAD Patching
PAD (Platform-Agnostic Deployment) patching modifies the build output to be compatible with container-based deployment platforms. This is essential for deploying Mendix applications to Kubernetes, Cloud Foundry, or other container orchestrators.
The build command applies six patches automatically after MxBuild produces the PAD output:
| # | Patch | Applies To | Description |
|---|---|---|---|
| 1 | Set bin/start execute permission | All versions | ZIP extraction does not preserve the +x flag. This patch runs chmod 0755 on bin/start so the container can execute the start script. |
| 2 | Fix Dockerfile CMD (start.sh -> start) | 11.6.x only | MxBuild 11.6.x generates CMD ["./bin/start.sh"] but the actual script is named start (no .sh extension). |
| 3 | Remove config argument | All versions | Removes the "etc/Default" argument from the CMD line. The bin/start script accumulates args with CONFIG_FILES="$CONFIG_FILES $1", which produces a leading space (" etc/Default") that the runtime rejects. Without the argument, bin/start uses its own DEFAULT_CONF variable. |
| 4 | Replace deprecated base image | All versions | Replaces FROM openjdk:21 with FROM eclipse-temurin:21-jre. The openjdk Docker images are deprecated and no longer maintained. |
| 5 | Add HEALTHCHECK | All versions | Inserts a HEALTHCHECK instruction before the CMD line: `curl -f http://localhost:8080/ |
| 6 | Bind admin API to all interfaces | All versions | Appends admin.addresses = ["*"] to the PAD config file (etc/Default). Without this, the admin API (port 8090) only listens on localhost inside the container and is unreachable from outside even when the port is mapped. |
Patches are applied idempotently – if a patch has already been applied (e.g., from a previous build), it is skipped. The build output shows the status of each patch:
Applying patches...
[applied] Set bin/start execute permission
[skipped] Fix Dockerfile CMD (start.sh -> start)
[applied] Remove config arg from Dockerfile CMD
[applied] Replace deprecated openjdk base image
[applied] Add HEALTHCHECK instruction
[applied] Bind admin API to all interfaces
Validation with mx check
To validate a project without building:
mxcli docker check -p app.mpr
This runs mx check against the project and reports any errors:
Checking app for errors...
The app contains: 0 errors.
Auto-Download mxbuild
mxcli automatically downloads the correct mxbuild version for your project:
# Explicit setup
mxcli setup mxbuild -p app.mpr
# Or let docker build handle it automatically
mxcli docker build -p app.mpr
The downloaded mxbuild is cached at ~/.mxcli/mxbuild/{version}/ for reuse.
mxcli docker check
The mxcli docker check command validates a Mendix project without building it. It runs the Mendix mx check tool against the project and reports errors, warnings, and deprecations.
Usage
mxcli docker check -p app.mpr
What It Does
- Resolves the
mxbinary – Looks formxin the mxbuild directory, PATH, or cached installations at~/.mxcli/mxbuild/*/modeler/mx - Runs
mx check– Executes the Mendix project checker against the.mprfile - Reports results – Outputs errors, warnings, and deprecations to the console
If the project has errors, the command exits with a non-zero status code.
Auto-Download
If mxbuild is not installed locally, you can download it first:
# Explicit setup (downloads matching mxbuild version)
mxcli setup mxbuild -p app.mpr
# Then check
mxcli docker check -p app.mpr
The downloaded mxbuild (including the mx binary) is cached at ~/.mxcli/mxbuild/{version}/ for reuse.
Example Output
A clean project:
Using mx: /home/vscode/.mxcli/mxbuild/11.6.3.12345/modeler/mx
Checking project /workspaces/app/app.mpr...
The app contains: 0 errors.
Project check passed.
A project with errors:
Using mx: /home/vscode/.mxcli/mxbuild/11.6.3.12345/modeler/mx
Checking project /workspaces/app/app.mpr...
CE0001: Entity 'MyModule.Customer' has no access rules configured.
CE0463: Widget definition changed for 'DataGrid2'.
The app contains: 2 errors.
project check failed: exit status 1
When to Use
mxcli docker check is faster than a full build (mxcli docker build) because it only validates the project structure without compiling or packaging. This makes it useful for:
- Quick validation after making MDL changes
- Pre-build gate –
mxcli docker buildrunsmx checkautomatically before building (use--skip-checkto bypass) - CI pipelines – fail fast on invalid projects before spending time on a full build
- Iterative development – check after each MDL script execution to catch errors early
Integration with mxcli docker build
The mxcli docker build command runs mx check automatically as a pre-build step. If the check fails, the build is aborted. You can skip this with:
mxcli docker build -p app.mpr --skip-check
Integration with the TUI
When using mxcli in interactive REPL mode, the TUI can auto-check the project on file changes, giving immediate feedback on whether MDL modifications introduced errors.
Related Pages
- Docker Build – Building a Mendix application with PAD patching
- Docker Run – Building and running the app in Docker
- Testing – MDL test framework
- Linting – MDL lint rules
mxcli docker run
The mxcli docker run command runs a Mendix application in a Docker container, providing a local runtime environment for testing and development.
Usage
mxcli docker run -p app.mpr
What It Does
- Builds the application if no build artifact exists
- Starts a Docker container with the Mendix runtime
- Configures the runtime with the project’s settings (database, ports, etc.)
- Exposes the application on the configured HTTP port
Prerequisites
- Docker must be installed and running
- The project must be buildable (no errors in
mxcli docker check) - A PostgreSQL database must be available (Docker can provide one)
Runtime Configuration
The runtime uses configuration from the project’s settings. You can view and modify these with:
SHOW SETTINGS;
DESCRIBE SETTINGS;
ALTER SETTINGS CONFIGURATION 'default' DatabaseType = 'POSTGRESQL';
ALTER SETTINGS CONFIGURATION 'default' HttpPortNumber = '8080';
Checking Project Health
Validate the project before running:
# Check for errors
mxcli docker check -p app.mpr
Use with OQL
Once the application is running, you can query it with OQL:
mxcli oql -p app.mpr "SELECT * FROM Sales.Customer"
See OQL Queries for details.
OQL Queries
The mxcli oql command executes OQL (Object Query Language) queries against a running Mendix runtime via the M2EE admin API. OQL is Mendix’s query language for retrieving data from the runtime, similar to SQL but operating on Mendix entities rather than database tables.
Usage
mxcli oql -p app.mpr "SELECT * FROM Sales.Customer"
Prerequisites
- A Mendix application must be running (via
mxcli docker runor another deployment) - The M2EE admin API must be accessible
- The project file is needed to resolve entity and attribute names
Query Syntax
OQL uses Mendix entity names and attribute names, not database table names:
# Select all customers
mxcli oql -p app.mpr "SELECT * FROM Sales.Customer"
# Select specific attributes
mxcli oql -p app.mpr "SELECT Name, Email FROM Sales.Customer"
# Filter with WHERE
mxcli oql -p app.mpr "SELECT * FROM Sales.Order WHERE Status = 'Open'"
# Join entities via associations
mxcli oql -p app.mpr "SELECT o.OrderNumber, c.Name FROM Sales.Order o JOIN Sales.Customer c ON o.Sales.Order_Customer = c"
# Aggregation
mxcli oql -p app.mpr "SELECT COUNT(*) FROM Sales.Customer"
Difference from Catalog Queries
| Feature | OQL (mxcli oql) | Catalog (SELECT FROM CATALOG.*) |
|---|---|---|
| Data source | Running Mendix runtime | Project metadata (MPR file) |
| Queries | Instance data (rows) | Model structure (entities, microflows) |
| Requires | Running application | Only the MPR file |
| Language | OQL (Mendix-specific) | SQL (SQLite) |
- Use OQL to query actual data in a running application
- Use catalog queries to analyze the project structure and metadata
Use Cases
Verifying Imported Data
After running IMPORT FROM, verify the data was imported correctly:
mxcli oql -p app.mpr "SELECT COUNT(*) FROM HR.Employee"
mxcli oql -p app.mpr "SELECT Name, Email FROM HR.Employee LIMIT 10"
Debugging Business Logic
Check data state while debugging microflows:
mxcli oql -p app.mpr "SELECT * FROM Sales.Order WHERE Status = 'Draft'"
Data Exploration
Explore data patterns in a running application:
mxcli oql -p app.mpr "SELECT Status, COUNT(*) FROM Sales.Order GROUP BY Status"
Dev Container Setup
mxcli supports development inside VS Code Dev Containers, providing a consistent, pre-configured development environment with all tools installed.
What Is a Dev Container?
A dev container is a Docker-based development environment defined by a devcontainer.json configuration file. When you open a project in VS Code with the Dev Containers extension, it builds and starts a container with all development tools pre-installed.
mxcli in Dev Containers
When running inside a dev container, mxcli tools and the mx binary are available at known paths:
| Tool | Path |
|---|---|
| mxcli | Available on PATH |
| mx binary | ~/.mxcli/mxbuild/{version}/modeler/mx |
| mxbuild | ~/.mxcli/mxbuild/{version}/ |
Setting Up mxbuild
To download the correct mxbuild version for your project:
mxcli setup mxbuild -p app.mpr
This downloads mxbuild to ~/.mxcli/mxbuild/{version}/ and makes the mx tool available for validation and project creation.
Validating Projects
# Find the mx binary
MX=~/.mxcli/mxbuild/*/modeler/mx
# Check a project
$MX check /path/to/app.mpr
# Create a fresh project for testing
cd /tmp/test-workspace
$MX create-project
Project Initialization
Initialize mxcli for your project inside the dev container:
# Initialize with Claude Code support
mxcli init -p app.mpr
# This creates:
# - .claude/settings.json
# - .claude/commands/
# - .claude/lint-rules/
# - .ai-context/skills/
# - CLAUDE.md
# - VS Code MDL extension (auto-installed)
Docker-in-Docker
For mxcli docker build, mxcli docker run, and mxcli test (which require Docker), the dev container must have Docker-in-Docker support enabled. This is typically configured in devcontainer.json:
{
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}
}
Typical Dev Container Workflow
- Open the project in VS Code
- VS Code prompts to reopen in dev container
- Inside the container, run
mxcli setup mxbuild -p app.mpr - Run
mxcli initto set up AI assistant integration - Use
mxclicommands as normal (REPL, exec, lint, test, etc.)
VS Code Extension
The MDL extension for VS Code provides a rich editing experience for .mdl files, bringing IDE-level support for Mendix Definition Language. The extension communicates with the mxcli lsp language server to provide real-time diagnostics, code completion, hover documentation, and navigation.
Feature Summary
| Feature | Description |
|---|---|
| Syntax highlighting | MDL keyword, string, comment, and identifier coloring |
| Parse diagnostics | Real-time error reporting as you type |
| Semantic diagnostics | Reference validation on save (entities, microflows, pages) |
| Code completion | Context-aware keyword and snippet suggestions |
| Hover | View MDL definitions by hovering over Module.Name references |
| Go-to-definition | Ctrl+click to open element source as a virtual document |
| Document outline | Navigate MDL statements via the Outline panel |
| Folding | Collapse statement blocks and widget trees |
| Context menu | Run File, Run Selection, and Check File commands |
| Project tree | Browse modules and documents in a Mendix-specific tree view |
How It Works
The extension spawns mxcli lsp --stdio as a background process and communicates via the Language Server Protocol over standard I/O. This means all analysis, completion, and navigation is performed by the same engine that powers the mxcli command line.
┌──────────────┐ stdio ┌──────────────────┐ ┌────────────┐
│ VS Code │◀──────────────▶│ mxcli lsp │────▶│ .mpr file │
│ Extension │ LSP JSON-RPC │ --stdio │ │ │
└──────────────┘ └──────────────────┘ └────────────┘
Requirements
- VS Code 1.80 or later (or any VS Code fork such as Cursor)
- mxcli binary on
PATHor configured via themdl.mxcliPathsetting - A Mendix project (
.mprfile) for semantic features
Settings
| Setting | Default | Description |
|---|---|---|
mdl.mxcliPath | mxcli | Path to the mxcli executable |
mdl.mprPath | (auto-discovered) | Path to .mpr file; if empty, the extension searches the workspace |
Getting Started
The fastest way to get started is mxcli init, which automatically installs the extension into your project. See Installation for details, or jump to specific features:
Installation
There are three ways to install the VS Code MDL extension.
Option 1: mxcli init (Recommended)
Running mxcli init on a Mendix project automatically installs the VS Code extension along with skills, commands, and configuration files:
mxcli init /path/to/my-mendix-project
The command copies the bundled .vsix file into the project and installs it into VS Code. This is the recommended approach because it also sets up the full AI-assisted development environment.
Option 2: Manual VSIX Install
If you already have the VSIX file (e.g., from the mxcli build artifacts), install it directly:
code --install-extension vscode-mdl/vscode-mdl-*.vsix
Or from the VS Code Command Palette:
- Open Extensions: Install from VSIX… (Ctrl+Shift+P)
- Navigate to the
.vsixfile - Click Install
Option 3: Build from Source
If you are developing or modifying the extension:
# Requires bun (not npm/node)
cd vscode-mdl
bun install
bun run compile
# Package as VSIX
make vscode-ext
# Install the built VSIX
make vscode-install
Note: The VS Code extension build toolchain uses bun, not npm or node. The Makefile targets
make vscode-extandmake vscode-installhandle this automatically.
Verifying the Installation
After installation, open any .mdl file in VS Code. You should see:
- Syntax highlighting – MDL keywords like
CREATE,SHOW, andDESCRIBEare colored - Parse diagnostics – Syntax errors appear as red underlines immediately
- Status bar – The MDL language server status appears in the bottom bar
Configuring the Extension
Open VS Code settings (Ctrl+,) and search for “MDL”:
| Setting | Default | Description |
|---|---|---|
mdl.mxcliPath | mxcli | Path to the mxcli binary. Set this if mxcli is not on your PATH. |
mdl.mprPath | (empty) | Path to the .mpr file. If empty, the extension auto-discovers it by searching the workspace root. |
Example settings.json:
{
"mdl.mxcliPath": "./mxcli",
"mdl.mprPath": "./app.mpr"
}
Dev Container Setup
When using mxcli init, a .devcontainer/ configuration is also created. Opening the project in VS Code and choosing Reopen in Container gives you a sandboxed environment with:
| Component | Purpose |
|---|---|
| mxcli | Mendix CLI binary (copied into the project) |
| MxBuild / mx | Project validation and building |
| JDK 21 | Required by MxBuild |
| Docker-in-Docker | Running Mendix apps locally |
| Node.js | Playwright testing support |
| PostgreSQL client | Database connectivity |
| Claude Code | AI coding assistant (auto-installed) |
The VS Code extension is automatically available inside the dev container.
Syntax Highlighting and Diagnostics
The VS Code extension provides three layers of language feedback: syntax highlighting, parse diagnostics, and semantic diagnostics.
Syntax Highlighting
MDL files (.mdl) receive full syntax coloring based on a TextMate grammar:
| Token Type | Examples | Color Theme Category |
|---|---|---|
| Keywords | CREATE, SHOW, DESCRIBE, ALTER, GRANT | keyword |
| Types | String, Integer, Boolean, DateTime | support.type |
| Strings | 'Hello World' | string |
| Numbers | 100, 3.14 | constant.numeric |
| Comments | -- line comment, /** doc comment */ | comment |
| Identifiers | MyModule.Customer | variable |
| Operators | =, !=, AND, OR | keyword.operator |
Highlighting works immediately on file open – no project or language server connection is required.
Parse Diagnostics (Real-Time)
As you type, the extension sends the file content to the MDL language server, which runs the ANTLR4 parser and reports syntax errors in real time. These appear as:
- Red underlines on the offending tokens
- Error entries in the Problems panel (Ctrl+Shift+M)
- Inline hover messages when you point at the underlined text
Parse diagnostics catch issues like:
- Missing semicolons
- Unmatched parentheses or braces
- Invalid keyword combinations
- Malformed qualified names
Example error:
Line 3, Col 42: mismatched input ')' expecting ','
Parse diagnostics run on every keystroke (debounced) and do not require a Mendix project file.
Semantic Diagnostics (On Save)
When you save a .mdl file, the language server performs deeper validation against the actual Mendix project (.mpr file). Semantic diagnostics check:
- Entity references – does
MyModule.Customeractually exist? - Attribute references – does the entity have the named attribute?
- Microflow references – do called microflows exist with correct signatures?
- Association references – are FROM/TO entities valid?
- Module existence – does the referenced module exist in the project?
Semantic diagnostics require:
- A valid
mdl.mprPathsetting (or auto-discovered.mprfile) - The
mxcli lspserver to be running
These diagnostics appear as warnings or errors in the Problems panel, identical in appearance to parse errors but typically with more descriptive messages referencing the project state.
Diagnostic Severity Levels
| Severity | Meaning |
|---|---|
| Error | Syntax error or invalid reference that would prevent execution |
| Warning | Valid syntax but potentially problematic (e.g., unused variable) |
| Information | Suggestion or style recommendation |
Equivalent CLI Commands
The same checks available in the extension can be run from the command line:
# Syntax check only (no project needed)
mxcli check script.mdl
# Syntax + reference validation
mxcli check script.mdl -p app.mpr --references
Completion, Hover, Go-to-Definition
The VS Code extension provides IntelliSense features powered by the MDL language server (mxcli lsp --stdio).
Code Completion
Press Ctrl+Space or start typing to trigger context-aware completions:
Keyword Completion
At the start of a statement, the extension suggests top-level MDL keywords:
CREATE SHOW DESCRIBE ALTER DROP GRANT REVOKE SEARCH REFRESH ...
After a keyword, context-appropriate follow-up keywords appear:
CREATE |
├── ENTITY
├── ASSOCIATION
├── ENUMERATION
├── MICROFLOW
├── PAGE
├── SNIPPET
├── MODULE
├── MODULE ROLE
├── USER ROLE
└── WORKFLOW
Snippet Completion
Common patterns are offered as multi-line snippets. For example, typing entity and selecting the snippet inserts:
CREATE PERSISTENT ENTITY ${1:Module}.${2:EntityName} (
${3:Name}: ${4:String(200)}
);
Reference Completion
When the language server has a loaded project, qualified name references offer completions from the actual model:
- Entity names after
FROM,TO, and in data source expressions - Module names after
INand as the first part of qualified names - Attribute names inside entity contexts
- Microflow names after
CALL MICROFLOW
Hover
Hover over any Module.Name qualified reference to see its MDL definition in a tooltip. For example, hovering over Sales.Customer in:
RETRIEVE $c FROM Sales.Customer WHERE ...
Displays the entity definition:
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200) NOT NULL,
Email: String(200),
IsActive: Boolean DEFAULT true
);
Hover works for entities, microflows, pages, enumerations, associations, and snippets.
Go-to-Definition
Ctrl+Click (or F12) on a qualified reference opens the element’s MDL source as a virtual read-only document. The definition is generated on-the-fly by describing the element from the project.
For example, Ctrl+clicking Sales.ProcessOrder opens a virtual document showing the full microflow definition with all its activities, conditions, and flows.
This also works from the terminal: qualified names like MyModule.EntityName in mxcli output are rendered as clickable links that open the definition.
Document Symbols and Outline
The Outline panel (View > Outline) shows all MDL statements in the current file as a navigable tree:
├── CREATE ENTITY Sales.Customer
├── CREATE ENTITY Sales.Order
├── CREATE ASSOCIATION Sales.Order_Customer
├── CREATE MICROFLOW Sales.ProcessOrder
└── CREATE PAGE Sales.CustomerOverview
Click any entry to jump to that statement in the editor.
Folding
Statement blocks can be collapsed:
CREATE MICROFLOW ... BEGIN ... END;– fold the microflow bodyCREATE PAGE ... { ... }– fold the widget treeIF ... THEN ... END IF;– fold conditional blocksLOOP ... BEGIN ... END LOOP;– fold loop bodies/** ... */– fold documentation comments
Project Tree
The VS Code extension provides a Mendix Project Tree view that mirrors the App Explorer in Mendix Studio Pro, allowing you to browse modules and documents without leaving VS Code.
Accessing the Tree
The project tree appears in the Explorer sidebar under the “Mendix Project” section. If it does not appear, check that:
- The extension is installed and enabled
- A
.mprfile is present in the workspace (or configured viamdl.mprPath) - The
mxclibinary is accessible
Tree Structure
The tree is organized by module, with documents grouped by type:
Mendix Project
├── MyFirstModule
│ ├── Domain Model
│ │ ├── Customer
│ │ ├── Order
│ │ └── Product
│ ├── Microflows
│ │ ├── ACT_Customer_Create
│ │ ├── ACT_Customer_Delete
│ │ └── ProcessOrder
│ ├── Pages
│ │ ├── Customer_Overview
│ │ ├── Customer_NewEdit
│ │ └── Order_Overview
│ ├── Snippets
│ │ └── NavigationMenu
│ ├── Enumerations
│ │ └── OrderStatus
│ └── Nanoflows
│ └── NF_ValidateInput
├── Administration
│ └── ...
└── System
└── ...
Interactions
- Click a document to open its MDL source as a virtual read-only document (equivalent to
DESCRIBEin the REPL) - Expand a module to see all document types
- Expand a document type to see individual documents
Refreshing
The tree refreshes automatically when the project file changes. You can also trigger a manual refresh from the tree view toolbar.
Relationship to SHOW Commands
The tree is backed by the same metadata that powers the SHOW commands:
| Tree Level | Equivalent MDL Command |
|---|---|
| Module list | SHOW MODULES |
| Entities in a module | SHOW ENTITIES IN MyModule |
| Microflows in a module | SHOW MICROFLOWS IN MyModule |
| Pages in a module | SHOW PAGES IN MyModule |
| Full structure | SHOW STRUCTURE IN MyModule DEPTH 2 |
Context Menu Commands
The VS Code extension adds three commands to the editor context menu (right-click) for .mdl files. These commands provide quick access to running and validating MDL without switching to a terminal.
Run File
Right-click > MDL: Run File
Executes the entire .mdl file against the configured Mendix project. This is equivalent to:
mxcli exec script.mdl -p app.mpr
Output appears in the integrated terminal. Any errors from execution (missing references, invalid operations) are displayed inline.
Check File
Right-click > MDL: Check File
Validates the file’s syntax without executing it. This is equivalent to:
mxcli check script.mdl
If a project is configured, reference validation is also performed:
mxcli check script.mdl -p app.mpr --references
Check results appear in the Problems panel alongside the live diagnostics from the language server.
Run Selection
Right-click > MDL: Run Selection
Executes only the currently selected text as MDL. This is useful for:
- Running a single
CREATEstatement from a larger script - Testing a
SHOWorDESCRIBEcommand interactively - Executing a
REFRESH CATALOGbefore running queries
The selection must contain one or more complete MDL statements (terminated by ;).
Keyboard Shortcuts
The commands do not have default keyboard shortcuts, but you can bind them in VS Code:
- Open Keyboard Shortcuts (Ctrl+K Ctrl+S)
- Search for “MDL”
- Assign your preferred key bindings
Terminal Integration
All commands use the VS Code integrated terminal. Output from mxcli includes clickable links for qualified names – Ctrl+click on MyModule.EntityName in the terminal output to open the element’s definition.
LSP Server
The MDL language server implements the Language Server Protocol (LSP), enabling any editor with LSP support to provide rich MDL editing features. The server is built into the mxcli binary – no separate installation is needed.
Overview
The language server is started with:
mxcli lsp --stdio
It communicates over standard I/O using JSON-RPC, the standard LSP transport. The server provides:
- Real-time diagnostics from the ANTLR4 parser
- Semantic diagnostics validated against the Mendix project
- Code completion with keywords, snippets, and project references
- Hover showing MDL definitions for qualified names
- Go-to-definition opening element source as virtual documents
- Document symbols for outline and breadcrumb navigation
- Folding ranges for collapsible statement blocks
Architecture
┌──────────────┐ ┌─────────────────────┐
│ Editor │◀── LSP JSON-RPC ─▶│ mxcli lsp │
│ (VS Code, │ over stdio │ │
│ Neovim, │ │ ┌───────────────┐ │
│ Emacs) │ │ │ ANTLR4 Parser │ │
└──────────────┘ │ └───────────────┘ │
│ ┌───────────────┐ │
│ │ MPR Reader │ │
│ └───────────────┘ │
│ ┌───────────────┐ │
│ │ Catalog │ │
│ └───────────────┘ │
└─────────────────────┘
The server is implemented in cmd/mxcli/lsp.go. It reuses the same ANTLR4 parser, MPR reader, and catalog components that power the mxcli CLI.
Project Discovery
On startup, the server auto-discovers the Mendix project:
- Check the
mdl.mprPathclient configuration - Search the workspace root for
*.mprfiles - If found, load the project for semantic features
If no project is found, the server still provides syntax highlighting and parse diagnostics, but semantic features (reference validation, hover definitions, completion from project) are disabled.
Related Pages
- Protocol Details – stdio transport and initialization
- Capabilities – full list of supported LSP methods
- Other Editors – Neovim, Emacs, and generic LSP client setup
Protocol
The MDL language server uses the standard LSP stdio transport: JSON-RPC messages are exchanged over the process’s standard input and standard output.
Starting the Server
mxcli lsp --stdio
The --stdio flag is required. The server reads LSP requests from stdin and writes responses to stdout. Diagnostic logging goes to stderr.
Initialization Handshake
The client initiates the connection with an initialize request. The server responds with its capabilities:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"capabilities": {},
"rootUri": "file:///path/to/project",
"initializationOptions": {
"mprPath": "app.mpr"
}
}
}
The initializationOptions.mprPath is optional. If provided, the server uses it to locate the Mendix project. Otherwise, it searches the rootUri for .mpr files.
The server responds with:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"capabilities": {
"textDocumentSync": 1,
"completionProvider": { "triggerCharacters": [".", " "] },
"hoverProvider": true,
"definitionProvider": true,
"documentSymbolProvider": true,
"foldingRangeProvider": true,
"diagnosticProvider": {
"interFileDependencies": false,
"workspaceDiagnostics": false
}
}
}
}
After receiving the response, the client sends initialized to complete the handshake.
Document Synchronization
The server uses full document sync (textDocumentSync: 1). On every change, the client sends the complete document content:
textDocument/didOpen– file opened, triggers initial parsetextDocument/didChange– content changed, triggers re-parse and diagnosticstextDocument/didSave– file saved, triggers semantic validationtextDocument/didClose– file closed, clears diagnostics
Diagnostics
Diagnostics are pushed from the server to the client via textDocument/publishDiagnostics notifications. Two levels of diagnostics are reported:
- Parse diagnostics – sent after every
didChange, based on ANTLR4 parser errors - Semantic diagnostics – sent after
didSave, based on reference validation against the project
Shutdown
The client sends shutdown followed by exit to terminate the server cleanly.
Capabilities
The MDL language server supports the following LSP capabilities.
Supported Methods
| LSP Method | Feature | Notes |
|---|---|---|
textDocument/publishDiagnostics | Parse and semantic error reporting | Push-based; parse on change, semantic on save |
textDocument/completion | Code completion | Keywords, snippets, and project references |
textDocument/hover | Hover documentation | MDL definitions for qualified names |
textDocument/definition | Go-to-definition | Opens element source as virtual document |
textDocument/documentSymbol | Document outline | All MDL statements in the file |
textDocument/foldingRange | Code folding | Statement blocks, widget trees, comments |
Document Synchronization
| Method | Supported |
|---|---|
textDocument/didOpen | Yes |
textDocument/didChange | Yes (full sync) |
textDocument/didSave | Yes |
textDocument/didClose | Yes |
Completion Details
Trigger characters: . and (space).
Completion items include:
- Keywords –
CREATE,SHOW,DESCRIBE,ALTER,DROP,GRANT, etc. - Keyword sequences –
PERSISTENT ENTITY,MODULE ROLE,USER ROLE - Data types –
String,Integer,Boolean,DateTime,Decimal, etc. - Snippets – Multi-line templates for entity, microflow, page creation
- Project references – Module names, entity names, microflow names (requires project)
Diagnostics Details
Parse Diagnostics (on change)
- Syntax errors from the ANTLR4 lexer and parser
- Missing tokens, unexpected tokens, mismatched brackets
- Reported with exact line/column positions
Semantic Diagnostics (on save)
- Unresolved entity, microflow, or page references
- Invalid attribute names on known entities
- Module existence checks
- Requires a loaded Mendix project
Not Yet Supported
The following LSP capabilities are not currently implemented:
| LSP Method | Status |
|---|---|
textDocument/references | Planned |
textDocument/rename | Planned |
textDocument/formatting | Planned |
textDocument/codeAction | Planned |
textDocument/signatureHelp | Not planned |
workspace/symbol | Not planned |
Integration with Other Editors
The MDL language server works with any editor that supports the Language Server Protocol. This page covers setup for editors other than VS Code.
General Setup
All editors need to:
- Start
mxcli lsp --stdioas a subprocess - Communicate via JSON-RPC over stdin/stdout
- Associate the language server with
.mdlfiles
Neovim
Using nvim-lspconfig
Add the following to your Neovim configuration:
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
-- Define the MDL language server
if not configs.mdl then
configs.mdl = {
default_config = {
cmd = { 'mxcli', 'lsp', '--stdio' },
filetypes = { 'mdl' },
root_dir = function(fname)
return lspconfig.util.find_git_ancestor(fname)
or lspconfig.util.root_pattern('*.mpr')(fname)
end,
settings = {},
},
}
end
lspconfig.mdl.setup({})
File Type Detection
Add MDL file type recognition:
vim.filetype.add({
extension = {
mdl = 'mdl',
},
})
Syntax Highlighting
For basic syntax highlighting without a full TreeSitter grammar, create ~/.config/nvim/syntax/mdl.vim with keyword highlights for MDL reserved words, or use the TextMate grammar from the VS Code extension via a plugin like nvim-textmate.
Emacs
Using lsp-mode
(require 'lsp-mode)
(add-to-list 'lsp-language-id-configuration '(mdl-mode . "mdl"))
(lsp-register-client
(make-lsp-client
:new-connection (lsp-stdio-connection '("mxcli" "lsp" "--stdio"))
:activation-fn (lsp-activate-on "mdl")
:server-id 'mdl-ls))
(add-hook 'mdl-mode-hook #'lsp)
Using eglot (Built-in since Emacs 29)
(add-to-list 'eglot-server-programs
'(mdl-mode . ("mxcli" "lsp" "--stdio")))
(add-hook 'mdl-mode-hook #'eglot-ensure)
File Mode
Define a basic major mode for .mdl files:
(define-derived-mode mdl-mode prog-mode "MDL"
"Major mode for editing MDL (Mendix Definition Language) files."
(setq-local comment-start "-- ")
(setq-local comment-end ""))
(add-to-list 'auto-mode-alist '("\\.mdl\\'" . mdl-mode))
Sublime Text
Using LSP Package
Install the LSP package, then add to your LSP settings:
{
"clients": {
"mdl": {
"enabled": true,
"command": ["mxcli", "lsp", "--stdio"],
"selector": "source.mdl",
"schemes": ["file"]
}
}
}
Helix
Add to ~/.config/helix/languages.toml:
[[language]]
name = "mdl"
scope = "source.mdl"
file-types = ["mdl"]
language-servers = ["mdl-ls"]
comment-token = "--"
[language-server.mdl-ls]
command = "mxcli"
args = ["lsp", "--stdio"]
Generic LSP Client
For any editor with LSP support, the key configuration values are:
| Parameter | Value |
|---|---|
| Command | mxcli lsp --stdio |
| Transport | stdio |
| Language ID | mdl |
| File extensions | .mdl |
| Trigger characters | ., |
mxcli init
The mxcli init command prepares a Mendix project for AI-assisted development. It creates configuration files, installs skills, sets up a dev container, and optionally installs the VS Code extension.
Basic Usage
# Initialize with Claude Code (default)
mxcli init /path/to/my-mendix-project
# Initialize for a specific AI tool
mxcli init --tool cursor /path/to/my-mendix-project
# Initialize for multiple tools
mxcli init --tool claude --tool cursor /path/to/my-mendix-project
# Initialize for all supported tools
mxcli init --all-tools /path/to/my-mendix-project
# List supported tools
mxcli init --list-tools
Supported AI Tools
| Tool | Flag | Config Files Created |
|---|---|---|
| Claude Code (default) | --tool claude | .claude/settings.json, CLAUDE.md, commands, lint rules |
| Cursor | --tool cursor | .cursorrules |
| Continue.dev | --tool continue | .continue/config.json |
| Windsurf | --tool windsurf | .windsurfrules |
| Aider | --tool aider | .aider.conf.yml |
All tools also receive the universal files (AGENTS.md, .ai-context/).
What Happens During Init
- Detect the Mendix project – finds the
.mprfile in the target directory - Create universal AI context –
AGENTS.mdand.ai-context/skills/with MDL pattern guides - Create tool-specific configuration – based on the selected
--toolflag(s) - Set up dev container –
.devcontainer/with Dockerfile and configuration - Copy mxcli binary – places the mxcli executable in the project root
- Install VS Code extension – copies and installs the bundled
.vsixfile
Adding a Tool Later
To add support for an additional AI tool to an existing project:
mxcli add-tool cursor
This creates only the tool-specific files without overwriting existing configuration.
Related Pages
- What Gets Created – detailed directory listing
- Customizing Skills – modifying generated skill files
- Syncing with Updates – keeping files current after mxcli upgrades
What Gets Created
Running mxcli init creates the following directory structure in your Mendix project.
Universal Files (All Tools)
These files are shared by all AI tools:
your-mendix-project/
├── AGENTS.md # Comprehensive AI assistant guide
├── .ai-context/
│ ├── skills/ # MDL pattern guides
│ │ ├── write-microflows.md # Microflow syntax and patterns
│ │ ├── create-page.md # Page/widget syntax reference
│ │ ├── alter-page.md # ALTER PAGE in-place modifications
│ │ ├── overview-pages.md # CRUD page patterns
│ │ ├── master-detail-pages.md # Master-detail page patterns
│ │ ├── generate-domain-model.md # Entity/Association syntax
│ │ ├── check-syntax.md # Pre-flight validation checklist
│ │ ├── organize-project.md # Folders, MOVE, project structure
│ │ ├── manage-security.md # Roles, access control, GRANT/REVOKE
│ │ ├── manage-navigation.md # Navigation profiles, menus
│ │ ├── demo-data.md # Database/import/demo data
│ │ ├── xpath-constraints.md # XPath syntax in WHERE clauses
│ │ ├── database-connections.md # External database connections
│ │ ├── test-microflows.md # Test annotations and Docker setup
│ │ └── patterns-data-processing.md # Delta merge, batch processing
│ └── examples/ # Example MDL scripts
├── mxcli # CLI executable (copied)
└── .devcontainer/ # Dev container configuration
├── devcontainer.json
└── Dockerfile
Tool-Specific Files
Claude Code
.claude/
├── settings.json # Claude Code project settings
├── commands/ # Slash commands for Claude
│ └── mendix/ # Mendix-specific commands
└── lint-rules/ # Starlark lint rules
CLAUDE.md # Project context for Claude
Cursor
.cursorrules # Compact MDL reference and command guide
Continue.dev
.continue/
└── config.json # Custom commands and slash commands
Windsurf
.windsurfrules # MDL rules for Codeium's AI
Aider
.aider.conf.yml # YAML configuration for Aider
Dev Container Contents
The .devcontainer/ provides a sandboxed environment with:
| Component | Purpose |
|---|---|
| mxcli | Mendix CLI binary |
| MxBuild / mx | Project validation and building (auto-downloaded on first use) |
| JDK 21 (Adoptium) | Required by MxBuild |
| Docker-in-Docker | Running Mendix apps locally with mxcli docker |
| Node.js | Playwright testing support |
| PostgreSQL client | Database connectivity |
| Claude Code | AI coding assistant (auto-installed on container creation) |
Key paths inside the container:
~/.mxcli/mxbuild/{version}/modeler/mx # mx check / mx build
~/.mxcli/runtime/{version}/ # Mendix runtime (auto-downloaded)
./mxcli # Project-local mxcli binary
Skills Overview
The skill files in .ai-context/skills/ teach AI assistants how to generate correct MDL. Each skill covers a specific topic:
| Skill File | What It Teaches |
|---|---|
write-microflows.md | Microflow syntax, common mistakes, validation checklist |
create-page.md | Page/widget syntax reference |
overview-pages.md | CRUD page patterns (data grids, search, buttons) |
generate-domain-model.md | Entity, attribute, association, enumeration syntax |
manage-security.md | Module roles, user roles, GRANT/REVOKE patterns |
demo-data.md | Mendix ID system, association storage, data insertion |
check-syntax.md | Pre-flight validation checklist before execution |
AI assistants are instructed to read the relevant skill file before generating MDL for that topic.
Customizing Skills
The skill files generated by mxcli init are starting points. You can modify them to match your organization’s patterns, naming conventions, and architectural standards.
Skill File Location
Skills are stored in .ai-context/skills/ (universal, shared by all tools) and optionally in .claude/skills/ (Claude Code-specific).
Modifying Existing Skills
Edit any skill file to adjust the patterns the AI follows. Common customizations include:
Naming Conventions
Change the default naming patterns in generate-domain-model.md:
-- Default pattern
CREATE PERSISTENT ENTITY Module.Customer ( ... );
-- Your convention: prefix entities with 'tbl_'
CREATE PERSISTENT ENTITY Module.tbl_Customer ( ... );
Page Layout Standards
Modify create-page.md or overview-pages.md to use your organization’s standard layouts:
-- Default
CREATE PAGE Module.CustomerOverview (
Layout: Atlas_Core.Atlas_Default
) { ... }
-- Your standard layout
CREATE PAGE Module.CustomerOverview (
Layout: MyTheme.MainLayout
) { ... }
Security Patterns
Update manage-security.md with your standard role hierarchy:
-- Your organization's standard roles
CREATE MODULE ROLE Module.SystemAdmin DESCRIPTION 'Full system access';
CREATE MODULE ROLE Module.PowerUser DESCRIPTION 'Extended user access';
CREATE MODULE ROLE Module.StandardUser DESCRIPTION 'Normal user access';
CREATE MODULE ROLE Module.ReadOnly DESCRIPTION 'View-only access';
Adding New Skills
Create new .md files in .ai-context/skills/ for custom patterns:
.ai-context/skills/
├── write-microflows.md # Built-in
├── create-page.md # Built-in
├── my-api-integration.md # Custom: your REST API patterns
├── my-approval-workflow.md # Custom: approval workflow template
└── my-reporting-pattern.md # Custom: standard report pages
Skill File Structure
A good skill file includes:
- Title and purpose – what the skill covers
- Syntax reference – the MDL syntax for the topic
- Complete examples – working MDL that can be adapted
- Common mistakes – pitfalls to avoid
- Validation checklist – steps to verify correctness
Example custom skill:
# REST API Integration Pattern
## When to Use
When creating microflows that call external REST APIs.
## Pattern
\```sql
CREATE MICROFLOW Module.Call_ExternalAPI($param: String)
RETURNS String AS $response
FOLDER 'Integration'
BEGIN
-- Your standard REST call pattern here
...
END;
\```
## Common Mistakes
- Forgetting error handling for HTTP failures
- Not logging the request/response for debugging
Claude Code Commands
For Claude Code specifically, you can also add slash commands in .claude/commands/mendix/:
.claude/commands/mendix/
├── create-entity.md
├── create-overview.md
└── your-custom-command.md
These appear as /mendix:your-custom-command in the Claude Code interface.
Syncing with Updates
When you upgrade mxcli to a newer version, the skills, commands, and VS Code extension bundled with it may have changed. This page explains how to keep your project’s files in sync.
What Gets Updated
| Component | Source of Truth | Sync Target |
|---|---|---|
| Skills | reference/mendix-repl/templates/.claude/skills/ | .ai-context/skills/ |
| Commands | .claude/commands/mendix/ | .claude/commands/mendix/ |
| VS Code extension | vscode-mdl/vscode-mdl-*.vsix | Installed extension |
| Lint rules | Bundled Starlark rules | .claude/lint-rules/ |
Re-running mxcli init
The simplest way to sync is to re-run mxcli init:
mxcli init /path/to/my-mendix-project
This overwrites the generated skill files with the latest versions. Custom modifications to built-in skill files will be lost. To preserve customizations:
- Keep custom skills in separate files (e.g.,
my-custom-pattern.md) - Or use version control to merge changes
Build-Time Sync (for mxcli developers)
When building mxcli from source, make build automatically syncs all assets:
make build # Syncs everything
make sync-skills # Skills only
make sync-commands # Commands only
make sync-vsix # VS Code extension only
Checking Versions
To see which version of mxcli and its bundled assets you have:
mxcli version
Recommended Workflow
- Keep custom skills separate from built-in skills so re-syncing does not overwrite them
- Use version control for your
.ai-context/and.claude/directories - Re-run
mxcli initafter upgrading mxcli to pick up new skills and bug fixes - Review the diff after syncing to see what changed in the skill files
Quick Start
The modelsdk-go library provides programmatic access to Mendix projects from Go code. It is the underlying library that powers the mxcli CLI tool.
Overview
- Read Mendix project files (
.mpr) to inspect modules, entities, microflows, pages, and more - Write changes back to projects: create entities, attributes, associations, microflows, pages
- No cloud connectivity required – works directly with local
.mprfiles - No CGO required – uses a pure Go SQLite driver (
modernc.org/sqlite) - Automatic format detection – handles both MPR v1 (single file) and v2 (directory-based) formats
Minimal Example
package main
import (
"fmt"
"github.com/mendixlabs/mxcli"
)
func main() {
reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer reader.Close()
modules, _ := reader.ListModules()
for _, m := range modules {
fmt.Printf("Module: %s\n", m.Name)
}
}
Two Access Modes
| Mode | Function | Use Case |
|---|---|---|
| Read-only | modelsdk.Open(path) | Inspecting project structure, generating reports |
| Read-write | modelsdk.OpenForWriting(path) | Creating entities, microflows, pages |
Two API Levels
| Level | Package | Style |
|---|---|---|
| Low-level SDK | modelsdk (root) | Direct type construction and writer calls |
| High-level Fluent API | api/ | Builder pattern with method chaining |
The low-level SDK gives full control over model elements. The fluent API provides a simplified interface for common operations.
MPR File Format
Mendix projects are stored in .mpr files which are SQLite databases containing BSON-encoded model elements.
- MPR v1 (Mendix < 10.18): Single
.mprfile containing all model data - MPR v2 (Mendix >= 10.18):
.mprmetadata file +mprcontents/folder with individual documents
The library automatically detects and handles both formats.
Model Structure
Project
├── Modules
│ ├── Domain Model
│ │ ├── Entities
│ │ │ ├── Attributes
│ │ │ ├── Indexes
│ │ │ ├── Access Rules
│ │ │ ├── Validation Rules
│ │ │ └── Event Handlers
│ │ ├── Associations
│ │ └── Annotations
│ ├── Microflows
│ │ ├── Parameters
│ │ └── Activities & Flows
│ ├── Nanoflows
│ ├── Pages
│ │ ├── Widgets
│ │ └── Data Sources
│ ├── Layouts
│ ├── Snippets
│ ├── Enumerations
│ ├── Constants
│ ├── Scheduled Events
│ └── Java Actions
└── Project Documents
Comparison with Official SDK
| Feature | Mendix Model SDK (TypeScript) | modelsdk-go |
|---|---|---|
| Language | TypeScript/JavaScript | Go |
| Runtime | Node.js | Native binary |
| Cloud Required | Yes (Platform API) | No |
| Local Files | No | Yes |
| Real-time Collaboration | Yes | No |
| Read Operations | Yes | Yes |
| Write Operations | Yes | Yes |
| Type Safety | Yes (TypeScript) | Yes (Go) |
| CLI Tool | No | Yes (mxcli) |
| SQL-like DSL | No | Yes (MDL) |
Installation
Go Module
Install the library with go get:
go get github.com/mendixlabs/mxcli
Requirements
- Go 1.21+
- No CGO required – the project uses
modernc.org/sqlite(pure Go SQLite) and does not require a C compiler
Dependencies
The library pulls in the following key dependencies:
| Dependency | Purpose |
|---|---|
modernc.org/sqlite | Pure Go SQLite driver for reading .mpr files |
go.mongodb.org/mongo-driver | BSON parsing for Mendix document format |
These are resolved automatically by go get.
Verify Installation
Create a simple Go program to verify the library is working:
package main
import (
"fmt"
"github.com/mendixlabs/mxcli"
)
func main() {
reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer reader.Close()
version := reader.GetMendixVersion()
fmt.Printf("Mendix version: %s\n", version)
fmt.Printf("MPR format: v%d\n", reader.Version())
}
Run it:
go run main.go
Building from Source
If you want to build the mxcli CLI tool from source:
git clone https://github.com/mendixlabs/mxcli.git
cd mxcli
make build
The binary will be placed in bin/mxcli.
Reading a Project
Open a Mendix project file (.mpr) in read-only mode to inspect its structure and contents.
Opening a Project
reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer reader.Close()
The library automatically detects the MPR format version (v1 or v2) and handles both transparently.
Complete Example
package main
import (
"fmt"
"github.com/mendixlabs/mxcli"
)
func main() {
// Open a Mendix project
reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer reader.Close()
// List all modules
modules, _ := reader.ListModules()
for _, m := range modules {
fmt.Printf("Module: %s\n", m.Name)
}
// Get domain model for a module
dm, _ := reader.GetDomainModel(modules[0].ID)
for _, entity := range dm.Entities {
fmt.Printf(" Entity: %s\n", entity.Name)
for _, attr := range entity.Attributes {
fmt.Printf(" - %s: %s\n", attr.Name, attr.Type.GetTypeName())
}
}
// List microflows
microflows, _ := reader.ListMicroflows()
fmt.Printf("Total microflows: %d\n", len(microflows))
// List pages
pages, _ := reader.ListPages()
fmt.Printf("Total pages: %d\n", len(pages))
}
Reader Methods
Metadata
reader.Path() // Get file path
reader.Version() // Get MPR version (1 or 2)
reader.GetMendixVersion() // Get Mendix Studio Pro version
Modules
reader.ListModules() // List all modules
reader.GetModule(id) // Get module by ID
reader.GetModuleByName(name) // Get module by name
Domain Models
reader.ListDomainModels() // List all domain models
reader.GetDomainModel(moduleID) // Get domain model for module
Microflows and Nanoflows
reader.ListMicroflows() // List all microflows
reader.GetMicroflow(id) // Get microflow by ID
reader.ListNanoflows() // List all nanoflows
reader.GetNanoflow(id) // Get nanoflow by ID
Pages and Layouts
reader.ListPages() // List all pages
reader.GetPage(id) // Get page by ID
reader.ListLayouts() // List all layouts
reader.GetLayout(id) // Get layout by ID
Other Elements
reader.ListEnumerations() // List all enumerations
reader.ListConstants() // List all constants
reader.ListScheduledEvents() // List all scheduled events
reader.ExportJSON() // Export entire model as JSON
Running the Example
cd examples/read_project
go run main.go /path/to/MyApp.mpr
Data Flow
sequenceDiagram
participant App
participant SDK as modelsdk
participant Reader
participant Parser
participant FS as File System
App->>SDK: Open("project.mpr")
SDK->>Reader: NewReader(path)
Reader->>FS: Detect MPR version
alt MPR v1
Reader->>FS: Open SQLite database
else MPR v2
Reader->>FS: Read mprcontents/
end
SDK-->>App: Reader
App->>SDK: ListDomainModels()
SDK->>Reader: listUnitsByType("DomainModels$DomainModel")
Reader->>FS: Read unit contents
Reader->>Parser: parseDomainModel(bson)
Parser-->>Reader: *DomainModel
Reader-->>SDK: []*DomainModel
SDK-->>App: []*DomainModel
Units are loaded on-demand for performance. The reader uses lazy loading so that related documents are only parsed when accessed.
Modifying a Project
Make changes to a Mendix project file using the writer API. Always back up your .mpr file before modifying it.
Opening for Writing
writer, err := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer writer.Close()
The writer provides access to the underlying reader via writer.Reader().
Complete Example
package main
import (
"github.com/mendixlabs/mxcli"
)
func main() {
// Open for writing
writer, err := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer writer.Close()
reader := writer.Reader()
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)
// Create a new entity
customer := modelsdk.NewEntity("Customer")
writer.CreateEntity(dm.ID, customer)
// Add attributes
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Name", 200))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Email", 254))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewBooleanAttribute("IsActive"))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewDateTimeAttribute("CreatedDate", true))
// Create another entity
order := modelsdk.NewEntity("Order")
writer.CreateEntity(dm.ID, order)
// Create an association
assoc := modelsdk.NewAssociation("Customer_Order", customer.ID, order.ID)
writer.CreateAssociation(dm.ID, assoc)
}
Writer Methods
Modules
writer.CreateModule(module)
writer.UpdateModule(module)
writer.DeleteModule(id)
Entities
writer.CreateEntity(domainModelID, entity)
writer.UpdateEntity(domainModelID, entity)
writer.DeleteEntity(domainModelID, entityID)
Attributes
writer.AddAttribute(domainModelID, entityID, attribute)
Associations
writer.CreateAssociation(domainModelID, association)
writer.DeleteAssociation(domainModelID, associationID)
Microflows and Nanoflows
writer.CreateMicroflow(microflow)
writer.UpdateMicroflow(microflow)
writer.DeleteMicroflow(id)
writer.CreateNanoflow(nanoflow)
writer.UpdateNanoflow(nanoflow)
writer.DeleteNanoflow(id)
Pages and Layouts
writer.CreatePage(page)
writer.UpdatePage(page)
writer.DeletePage(id)
writer.CreateLayout(layout)
writer.UpdateLayout(layout)
writer.DeleteLayout(id)
Other
writer.CreateEnumeration(enumeration)
writer.CreateConstant(constant)
Helper Functions
// Create attributes
modelsdk.NewStringAttribute(name, length)
modelsdk.NewIntegerAttribute(name)
modelsdk.NewDecimalAttribute(name)
modelsdk.NewBooleanAttribute(name)
modelsdk.NewDateTimeAttribute(name, localize)
modelsdk.NewEnumerationAttribute(name, enumID)
// Create entities
modelsdk.NewEntity(name) // Persistable entity
modelsdk.NewNonPersistableEntity(name) // Non-persistable entity
// Create associations
modelsdk.NewAssociation(name, parentID, childID) // Reference (1:N)
modelsdk.NewReferenceSetAssociation(name, p, c) // Reference set (M:N)
// Create flows
modelsdk.NewMicroflow(name)
modelsdk.NewNanoflow(name)
// Create pages
modelsdk.NewPage(name)
// Generate IDs
modelsdk.GenerateID()
Data Flow
sequenceDiagram
participant App
participant SDK as modelsdk
participant Writer
participant FS as File System
App->>SDK: OpenForWriting("project.mpr")
SDK->>Writer: NewWriter(path)
Writer->>FS: Load existing units
SDK-->>App: Writer
App->>SDK: NewEntity()
SDK-->>App: *Entity
App->>SDK: AddEntity(dm, entity)
SDK->>Writer: SerializeEntity(entity)
Writer->>Writer: Generate BSON
App->>SDK: Save()
SDK->>Writer: Write all modified units
Writer->>FS: Write .mxunit files
Writer->>FS: Update .mpr metadata
Running the Example
cd examples/modify_project
go run main.go /path/to/MyApp.mpr
Warning: Always backup your .mpr file before modifying it.
Public API
The public API surface is defined in modelsdk.go at the package root. It provides convenience functions for opening projects and constructing model elements.
Entry Points
// Read-only access
reader, err := modelsdk.Open("/path/to/project.mpr")
defer reader.Close()
// Read-write access
writer, err := modelsdk.OpenForWriting("/path/to/project.mpr")
defer writer.Close()
Core Types
| Type | Description |
|---|---|
modelsdk.ID | Unique identifier for model elements (UUID) |
modelsdk.Module | Represents a Mendix module |
modelsdk.Project | Represents a Mendix project |
modelsdk.DomainModel | Contains entities and associations |
modelsdk.Entity | An entity in the domain model |
modelsdk.Attribute | An attribute of an entity |
modelsdk.Association | A relationship between entities |
modelsdk.Microflow | A microflow (server-side logic) |
modelsdk.Nanoflow | A nanoflow (client-side logic) |
modelsdk.Page | A page in the UI |
modelsdk.Layout | A page layout template |
Package Structure
github.com/mendixlabs/mxcli/
├── modelsdk.go # Main package with convenience functions
├── model/ # Core model types (ID, Module, Project, etc.)
├── api/ # High-level fluent API (builders)
│ ├── api.go # ModelAPI entry point
│ ├── domainmodels.go # EntityBuilder, AssociationBuilder
│ ├── enumerations.go # EnumerationBuilder
│ ├── microflows.go # MicroflowBuilder
│ ├── pages.go # PageBuilder, widget builders
│ └── modules.go # ModulesAPI
├── sdk/
│ ├── domainmodel/ # Domain model types (Entity, Attribute, Association)
│ ├── microflows/ # Microflow and Nanoflow types
│ ├── pages/ # Page, Layout, and Widget types
│ └── mpr/ # MPR file reader and writer
└── examples/ # Example applications
Reader API
The reader provides read-only access to all project elements:
reader, _ := modelsdk.Open("path/to/project.mpr")
defer reader.Close()
// Metadata
reader.Path() // Get file path
reader.Version() // Get MPR version (1 or 2)
reader.GetMendixVersion() // Get Mendix Studio Pro version
// Modules
reader.ListModules() // List all modules
reader.GetModule(id) // Get module by ID
reader.GetModuleByName(name) // Get module by name
// Domain Models
reader.ListDomainModels() // List all domain models
reader.GetDomainModel(moduleID) // Get domain model for module
// Microflows & Nanoflows
reader.ListMicroflows() // List all microflows
reader.GetMicroflow(id) // Get microflow by ID
reader.ListNanoflows() // List all nanoflows
reader.GetNanoflow(id) // Get nanoflow by ID
// Pages & Layouts
reader.ListPages() // List all pages
reader.GetPage(id) // Get page by ID
reader.ListLayouts() // List all layouts
reader.GetLayout(id) // Get layout by ID
// Other
reader.ListEnumerations() // List all enumerations
reader.ListConstants() // List all constants
reader.ListScheduledEvents() // List all scheduled events
reader.ExportJSON() // Export entire model as JSON
Writer API
The writer provides read-write access. Use writer.Reader() for read operations:
writer, _ := modelsdk.OpenForWriting("path/to/project.mpr")
defer writer.Close()
reader := writer.Reader()
// Module operations
writer.CreateModule(module)
writer.UpdateModule(module)
writer.DeleteModule(id)
// Entity operations
writer.CreateEntity(domainModelID, entity)
writer.UpdateEntity(domainModelID, entity)
writer.DeleteEntity(domainModelID, entityID)
// Attribute operations
writer.AddAttribute(domainModelID, entityID, attribute)
// Association operations
writer.CreateAssociation(domainModelID, association)
writer.DeleteAssociation(domainModelID, associationID)
// Microflow operations
writer.CreateMicroflow(microflow)
writer.UpdateMicroflow(microflow)
writer.DeleteMicroflow(id)
// Page operations
writer.CreatePage(page)
writer.UpdatePage(page)
writer.DeletePage(id)
// Other
writer.CreateEnumeration(enumeration)
writer.CreateConstant(constant)
Constructor Functions
Helper functions for creating model elements with proper defaults:
// Attributes
modelsdk.NewStringAttribute(name, length)
modelsdk.NewIntegerAttribute(name)
modelsdk.NewDecimalAttribute(name)
modelsdk.NewBooleanAttribute(name)
modelsdk.NewDateTimeAttribute(name, localize)
modelsdk.NewEnumerationAttribute(name, enumID)
// Entities
modelsdk.NewEntity(name) // Persistable
modelsdk.NewNonPersistableEntity(name) // Non-persistable
// Associations
modelsdk.NewAssociation(name, parentID, childID) // 1:N
modelsdk.NewReferenceSetAssociation(name, p, c) // M:N
// Flows
modelsdk.NewMicroflow(name)
modelsdk.NewNanoflow(name)
// Pages
modelsdk.NewPage(name)
// IDs
modelsdk.GenerateID()
Open / OpenForWriting
The two entry points for accessing a Mendix project from Go code.
Read-Only Access
Use modelsdk.Open() when you only need to inspect the project:
import modelsdk "github.com/mendixlabs/mxcli"
reader, err := modelsdk.Open("/path/to/MyApp.mpr")
if err != nil {
log.Fatal(err)
}
defer reader.Close()
// Query the project
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)
The reader provides all query methods (listing modules, entities, microflows, pages, etc.) but cannot modify the project. The .mpr file is opened with a read-only SQLite connection.
Read-Write Access
Use modelsdk.OpenForWriting() when you need to create or modify elements:
import modelsdk "github.com/mendixlabs/mxcli"
writer, err := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
log.Fatal(err)
}
defer writer.Close()
// Access read methods through the embedded reader
reader := writer.Reader()
modules, _ := reader.ListModules()
// Create new elements
entity := modelsdk.NewEntity("Customer")
writer.CreateEntity(domainModelID, entity)
The writer opens the .mpr file with a read-write SQLite connection. Always back up your .mpr file before modifying it – the library writes directly to the database.
Format Auto-Detection
Both functions automatically detect the MPR format version:
| Version | Mendix | Storage |
|---|---|---|
| v1 | < 10.18 | Single .mpr SQLite file with BSON blobs |
| v2 | >= 10.18 | .mpr metadata + mprcontents/ folder with .mxunit files |
No configuration is needed – the library inspects the file structure and selects the appropriate reader/writer implementation.
Error Handling
Both functions return an error if:
- The file path does not exist
- The file is not a valid SQLite database
- The file is not a recognized MPR format
- (For
OpenForWriting) the file is read-only or locked by another process
reader, err := modelsdk.Open("/path/to/project.mpr")
if err != nil {
// Handle: file not found, not a valid MPR, etc.
log.Fatalf("Failed to open project: %v", err)
}
Pure Go – No CGO Required
The library uses modernc.org/sqlite, a pure Go SQLite implementation. No C compiler, CGO, or system SQLite library is needed. This simplifies cross-compilation and deployment.
Reader Methods
Complete list of methods available on the reader returned by modelsdk.Open().
Metadata
reader.Path() // string -- file path of the .mpr
reader.Version() // int -- MPR version (1 or 2)
reader.GetMendixVersion() // string -- Mendix Studio Pro version (e.g., "11.6.0")
Modules
reader.ListModules() // ([]*model.Module, error) -- all modules
reader.GetModule(id) // (*model.Module, error) -- module by ID
reader.GetModuleByName(name) // (*model.Module, error) -- module by name
Domain Models
reader.ListDomainModels() // ([]*domainmodel.DomainModel, error) -- all domain models
reader.GetDomainModel(moduleID) // (*domainmodel.DomainModel, error) -- domain model for a module
The DomainModel struct contains:
Entities– slice of*domainmodel.Entity, each withAttributes,Indexes,ValidationRules,AccessRules, andEventHandlersAssociations– slice of*domainmodel.Association
Microflows and Nanoflows
reader.ListMicroflows() // ([]*microflows.Microflow, error) -- all microflows
reader.GetMicroflow(id) // (*microflows.Microflow, error) -- microflow by ID
reader.ListNanoflows() // ([]*microflows.Nanoflow, error) -- all nanoflows
reader.GetNanoflow(id) // (*microflows.Nanoflow, error) -- nanoflow by ID
Pages and Layouts
reader.ListPages() // ([]*pages.Page, error) -- all pages
reader.GetPage(id) // (*pages.Page, error) -- page by ID
reader.ListLayouts() // ([]*pages.Layout, error) -- all layouts
reader.GetLayout(id) // (*pages.Layout, error) -- layout by ID
Enumerations
reader.ListEnumerations() // ([]*model.Enumeration, error) -- all enumerations
Each enumeration contains:
Name– enumeration nameValues– slice of*model.EnumerationValuewithNameandCaption
Other Documents
reader.ListConstants() // ([]*model.Constant, error) -- all constants
reader.ListScheduledEvents() // ([]*model.ScheduledEvent, error) -- all scheduled events
Export
reader.ExportJSON() // ([]byte, error) -- entire model as JSON
Example: Full Project Scan
reader, _ := modelsdk.Open("/path/to/MyApp.mpr")
defer reader.Close()
fmt.Printf("Mendix version: %s\n", reader.GetMendixVersion())
modules, _ := reader.ListModules()
for _, m := range modules {
fmt.Printf("Module: %s\n", m.Name)
dm, _ := reader.GetDomainModel(m.ID)
for _, entity := range dm.Entities {
fmt.Printf(" Entity: %s (persistent: %v)\n", entity.Name, entity.Persistable)
for _, attr := range entity.Attributes {
fmt.Printf(" - %s: %s\n", attr.Name, attr.Type.GetTypeName())
}
}
}
microflows, _ := reader.ListMicroflows()
fmt.Printf("Total microflows: %d\n", len(microflows))
pages, _ := reader.ListPages()
fmt.Printf("Total pages: %d\n", len(pages))
Writer Methods
Complete list of methods available on the writer returned by modelsdk.OpenForWriting().
Accessing the Reader
The writer embeds a reader for querying the project:
writer, _ := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
defer writer.Close()
reader := writer.Reader()
modules, _ := reader.ListModules()
Modules
writer.CreateModule(module) // error -- create a new module
writer.UpdateModule(module) // error -- update module metadata
writer.DeleteModule(id) // error -- delete a module and its contents
Entities
writer.CreateEntity(domainModelID, entity) // error -- add entity to domain model
writer.UpdateEntity(domainModelID, entity) // error -- update an existing entity
writer.DeleteEntity(domainModelID, entityID) // error -- remove entity from domain model
Attributes
writer.AddAttribute(domainModelID, entityID, attribute) // error -- add attribute to entity
Associations
writer.CreateAssociation(domainModelID, association) // error -- create association
writer.DeleteAssociation(domainModelID, associationID) // error -- remove association
Microflows and Nanoflows
writer.CreateMicroflow(microflow) // error -- create a new microflow
writer.UpdateMicroflow(microflow) // error -- update an existing microflow
writer.DeleteMicroflow(id) // error -- delete a microflow
writer.CreateNanoflow(nanoflow) // error -- create a new nanoflow
writer.UpdateNanoflow(nanoflow) // error -- update an existing nanoflow
writer.DeleteNanoflow(id) // error -- delete a nanoflow
Pages and Layouts
writer.CreatePage(page) // error -- create a new page
writer.UpdatePage(page) // error -- update an existing page
writer.DeletePage(id) // error -- delete a page
writer.CreateLayout(layout) // error -- create a new layout
writer.UpdateLayout(layout) // error -- update an existing layout
writer.DeleteLayout(id) // error -- delete a layout
Enumerations and Constants
writer.CreateEnumeration(enumeration) // error -- create a new enumeration
writer.CreateConstant(constant) // error -- create a new constant
Helper Functions
The modelsdk package provides helper functions for creating common types:
// Entities
modelsdk.NewEntity(name) // *Entity -- persistent entity
modelsdk.NewNonPersistableEntity(name) // *Entity -- non-persistent entity
// Attributes
modelsdk.NewStringAttribute(name, length) // *Attribute
modelsdk.NewIntegerAttribute(name) // *Attribute
modelsdk.NewDecimalAttribute(name) // *Attribute
modelsdk.NewBooleanAttribute(name) // *Attribute
modelsdk.NewDateTimeAttribute(name, localize) // *Attribute
modelsdk.NewEnumerationAttribute(name, enumID) // *Attribute
modelsdk.NewAutoNumberAttribute(name) // *Attribute
// Associations
modelsdk.NewAssociation(name, parentID, childID) // *Association -- Reference (1:N)
modelsdk.NewReferenceSetAssociation(name, parentID, childID) // *Association -- ReferenceSet (M:N)
// Flows
modelsdk.NewMicroflow(name) // *Microflow
modelsdk.NewNanoflow(name) // *Nanoflow
// Pages
modelsdk.NewPage(name) // *Page
// IDs
modelsdk.GenerateID() // string -- new UUID v4
Example: Create an Entity with Attributes
writer, _ := modelsdk.OpenForWriting("/path/to/MyApp.mpr")
defer writer.Close()
reader := writer.Reader()
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)
// Create entity
customer := modelsdk.NewEntity("Customer")
customer.Documentation = "Customer master data"
customer.Location = model.Point{X: 100, Y: 200}
writer.CreateEntity(dm.ID, customer)
// Add attributes
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Name", 200))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewStringAttribute("Email", 254))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewBooleanAttribute("IsActive"))
writer.AddAttribute(dm.ID, customer.ID, modelsdk.NewDateTimeAttribute("CreatedDate", true))
// Create association
order := modelsdk.NewEntity("Order")
writer.CreateEntity(dm.ID, order)
assoc := modelsdk.NewAssociation("Customer_Order", customer.ID, order.ID)
writer.CreateAssociation(dm.ID, assoc)
Warning: Always back up your
.mprfile before modifying it. The writer operates directly on the SQLite database.
Fluent API
The api/ package provides a high-level, fluent builder API inspired by the Mendix Web Extensibility Model API. It simplifies common operations with method chaining and sensible defaults.
Setup
package main
import (
"github.com/mendixlabs/mxcli/api"
"github.com/mendixlabs/mxcli/sdk/mpr"
)
func main() {
writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer writer.Close()
// Create the high-level API
modelAPI := api.New(writer)
// Set the current module context
module, _ := modelAPI.Modules.GetModule("MyModule")
modelAPI.SetModule(module)
}
API Structure
classDiagram
class ModelAPI {
+writer *Writer
+reader *Reader
+currentModule *Module
+DomainModels *DomainModelsAPI
+Microflows *MicroflowsAPI
+Pages *PagesAPI
+Enumerations *EnumerationsAPI
+Modules *ModulesAPI
+New(writer) *ModelAPI
+SetModule(module) *ModelAPI
}
class EntityBuilder {
+Persistent() *EntityBuilder
+NonPersistent() *EntityBuilder
+WithStringAttribute(name, length) *EntityBuilder
+WithIntegerAttribute(name) *EntityBuilder
+Build() (*Entity, error)
}
class MicroflowBuilder {
+WithParameter(name, entity) *MicroflowBuilder
+WithStringParameter(name) *MicroflowBuilder
+ReturnsBoolean() *MicroflowBuilder
+Build() (*Microflow, error)
}
ModelAPI --> EntityBuilder : creates
ModelAPI --> MicroflowBuilder : creates
Namespaces
| Namespace | Description |
|---|---|
modelAPI.DomainModels | Create/modify entities, attributes, associations |
modelAPI.Enumerations | Create/modify enumerations and values |
modelAPI.Microflows | Create microflows with parameters and return types |
modelAPI.Pages | Create pages with widgets (DataView, TextBox, etc.) |
modelAPI.Modules | List and retrieve modules |
Creating Entities
// Create entity with fluent builder
customer, err := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
WithStringAttribute("Name", 100).
WithStringAttribute("Email", 254).
WithIntegerAttribute("Age").
WithBooleanAttribute("IsActive").
WithDateTimeAttribute("CreatedDate", true).
Build()
Creating Associations
_, err := modelAPI.DomainModels.CreateAssociation("Customer_Orders").
From("Customer").
To("Order").
OneToMany().
Build()
Creating Enumerations
_, err := modelAPI.Enumerations.CreateEnumeration("OrderStatus").
WithValue("Pending", "Pending").
WithValue("Processing", "Processing").
WithValue("Completed", "Completed").
WithValue("Cancelled", "Cancelled").
Build()
Creating Microflows
_, err := modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
WithParameter("Order", "MyModule.Order").
WithStringParameter("Message").
ReturnsBoolean().
Build()
Complete Example
package main
import (
"github.com/mendixlabs/mxcli/api"
"github.com/mendixlabs/mxcli/sdk/mpr"
)
func main() {
writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
panic(err)
}
defer writer.Close()
modelAPI := api.New(writer)
module, _ := modelAPI.Modules.GetModule("MyModule")
modelAPI.SetModule(module)
// Create entity with fluent builder
customer, _ := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
WithStringAttribute("Name", 100).
WithStringAttribute("Email", 254).
WithIntegerAttribute("Age").
WithBooleanAttribute("IsActive").
WithDateTimeAttribute("CreatedDate", true).
Build()
// Create another entity
order, _ := modelAPI.DomainModels.CreateEntity("Order").
Persistent().
WithDecimalAttribute("TotalAmount").
WithDateTimeAttribute("OrderDate", true).
Build()
// Create association between entities
_, _ = modelAPI.DomainModels.CreateAssociation("Customer_Orders").
From("Customer").
To("Order").
OneToMany().
Build()
// Create enumeration
_, _ = modelAPI.Enumerations.CreateEnumeration("OrderStatus").
WithValue("Pending", "Pending").
WithValue("Processing", "Processing").
WithValue("Completed", "Completed").
WithValue("Cancelled", "Cancelled").
Build()
// Create microflow
_, _ = modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
WithParameter("Order", "MyModule.Order").
WithStringParameter("Message").
ReturnsBoolean().
Build()
}
Package Files
| File | Purpose |
|---|---|
api/api.go | ModelAPI entry point with namespace access |
api/domainmodels.go | EntityBuilder, AssociationBuilder, AttributeBuilder |
api/enumerations.go | EnumerationBuilder, EnumValueBuilder |
api/microflows.go | MicroflowBuilder with parameters and return types |
api/pages.go | PageBuilder, widget builders |
api/modules.go | ModulesAPI for module retrieval |
ModelAPI Entry Point
The api package provides a high-level fluent API inspired by the Mendix Web Extensibility Model API. It wraps the lower-level reader/writer with builder patterns for common operations.
Creating the ModelAPI
import (
"github.com/mendixlabs/mxcli/api"
"github.com/mendixlabs/mxcli/sdk/mpr"
)
writer, _ := mpr.OpenForWriting("/path/to/MyApp.mpr")
defer writer.Close()
modelAPI := api.New(writer)
Setting the Module Context
Most operations require a module context. Set it before calling builders:
module, _ := modelAPI.Modules.GetModule("MyModule")
modelAPI.SetModule(module)
All subsequent Create* calls will create elements in this module.
Namespace Accessors
The ModelAPI struct provides access to domain-specific APIs through namespace accessors:
| Namespace | Accessor | Description |
|---|---|---|
| DomainModels | modelAPI.DomainModels | Create/modify entities, attributes, associations |
| Enumerations | modelAPI.Enumerations | Create/modify enumerations and their values |
| Microflows | modelAPI.Microflows | Create microflows with parameters and return types |
| Pages | modelAPI.Pages | Create pages with widgets |
| Modules | modelAPI.Modules | List and retrieve modules |
DomainModels Namespace
// Create an entity
entity, _ := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
WithStringAttribute("Name", 200).
Build()
// Create an association
assoc, _ := modelAPI.DomainModels.CreateAssociation("Customer_Orders").
From("Customer").
To("Order").
OneToMany().
Build()
Enumerations Namespace
enum, _ := modelAPI.Enumerations.CreateEnumeration("OrderStatus").
WithValue("Draft", "Draft").
WithValue("Active", "Active").
WithValue("Closed", "Closed").
Build()
Microflows Namespace
mf, _ := modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
WithParameter("Order", "MyModule.Order").
WithStringParameter("Note").
ReturnsBoolean().
Build()
Pages Namespace
page, _ := modelAPI.Pages.CreatePage("CustomerOverview").
WithTitle("Customer Overview").
Build()
Modules Namespace
// List all modules
modules, _ := modelAPI.Modules.ListModules()
// Get a specific module
module, _ := modelAPI.Modules.GetModule("MyModule")
MDL to Fluent API Mapping
| MDL Statement | Fluent API Method |
|---|---|
CREATE PERSISTENT ENTITY | DomainModels.CreateEntity().Persistent().Build() |
CREATE NON-PERSISTENT ENTITY | DomainModels.CreateEntity().NonPersistent().Build() |
CREATE ASSOCIATION | DomainModels.CreateAssociation().Build() |
CREATE ENUMERATION | Enumerations.CreateEnumeration().Build() |
CREATE MICROFLOW | Microflows.CreateMicroflow().Build() |
CREATE PAGE | Pages.CreatePage().Build() |
When to Use the Fluent API vs Direct SDK
| Use Case | Recommended Approach |
|---|---|
| Simple CRUD operations | Fluent API (less boilerplate) |
| Complex microflow construction | Direct SDK types (more control) |
| Bulk operations | Direct writer methods (batch efficiency) |
| Integration testing | Fluent API (readable test code) |
Builders
The api package provides fluent builder types for constructing Mendix model elements. Each builder follows the pattern: create, configure with chained methods, then call Build().
EntityBuilder
Creates entities with attributes, persistence settings, and documentation.
entity, err := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
WithStringAttribute("Name", 200).
WithStringAttribute("Email", 254).
WithIntegerAttribute("Age").
WithBooleanAttribute("IsActive").
WithDateTimeAttribute("CreatedDate", true).
WithDecimalAttribute("Revenue").
WithEnumerationAttribute("Status", "MyModule.CustomerStatus").
Build()
EntityBuilder Methods
| Method | Description |
|---|---|
Persistent() | Mark as persistent (stored in database) |
NonPersistent() | Mark as non-persistent (in-memory only) |
WithStringAttribute(name, length) | Add a string attribute |
WithIntegerAttribute(name) | Add an integer attribute |
WithLongAttribute(name) | Add a long attribute |
WithDecimalAttribute(name) | Add a decimal attribute |
WithBooleanAttribute(name) | Add a boolean attribute |
WithDateTimeAttribute(name, localize) | Add a datetime attribute |
WithAutoNumberAttribute(name) | Add an auto-number attribute |
WithEnumerationAttribute(name, enumRef) | Add an enumeration attribute |
Build() | Create the entity and write it to the project |
AssociationBuilder
Creates associations (relationships) between entities.
assoc, err := modelAPI.DomainModels.CreateAssociation("Customer_Orders").
From("Customer").
To("Order").
OneToMany().
Build()
AssociationBuilder Methods
| Method | Description |
|---|---|
From(entityName) | Set the FROM entity (FK owner) |
To(entityName) | Set the TO entity (referenced) |
OneToMany() | Reference type with Default owner (1:N) |
ManyToMany() | ReferenceSet type (M:N) |
Build() | Create the association and write it to the project |
EnumerationBuilder
Creates enumerations with named values and captions.
enum, err := modelAPI.Enumerations.CreateEnumeration("OrderStatus").
WithValue("Draft", "Draft Order").
WithValue("Pending", "Pending Approval").
WithValue("Completed", "Completed").
WithValue("Cancelled", "Cancelled").
Build()
EnumerationBuilder Methods
| Method | Description |
|---|---|
WithValue(name, caption) | Add a value with its display caption |
Build() | Create the enumeration and write it to the project |
MicroflowBuilder
Creates microflows with parameters and return types.
mf, err := modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
WithParameter("Order", "Sales.Order").
WithStringParameter("Message").
ReturnsBoolean().
Build()
MicroflowBuilder Methods
| Method | Description |
|---|---|
WithParameter(name, entityType) | Add an object parameter |
WithStringParameter(name) | Add a string parameter |
ReturnsBoolean() | Set return type to Boolean |
ReturnsString() | Set return type to String |
ReturnsVoid() | Set return type to Nothing (void) |
Build() | Create the microflow and write it to the project |
PageBuilder
Creates pages with title and layout configuration.
page, err := modelAPI.Pages.CreatePage("CustomerOverview").
WithTitle("Customer Overview").
Build()
PageBuilder Methods
| Method | Description |
|---|---|
WithTitle(title) | Set the page title |
Build() | Create the page and write it to the project |
Error Handling
All Build() methods return (result, error). Common errors include:
- Module context not set (
modelAPI.SetModule()not called) - Entity not found (for associations referencing nonexistent entities)
- Duplicate names within the same module
- Invalid attribute types or references
entity, err := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
Build()
if err != nil {
log.Fatalf("Failed to create entity: %v", err)
}
Examples
Complete fluent API examples for common tasks.
Setup
All examples start with the same setup:
package main
import (
"log"
"github.com/mendixlabs/mxcli/api"
"github.com/mendixlabs/mxcli/sdk/mpr"
)
func main() {
writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
log.Fatal(err)
}
defer writer.Close()
modelAPI := api.New(writer)
module, _ := modelAPI.Modules.GetModule("Sales")
modelAPI.SetModule(module)
// ... examples below
}
Create a Domain Model
// Create entity with multiple attribute types
customer, _ := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
WithStringAttribute("Name", 200).
WithStringAttribute("Email", 254).
WithIntegerAttribute("Age").
WithBooleanAttribute("IsActive").
WithDateTimeAttribute("CreatedDate", true).
Build()
// Create another entity
order, _ := modelAPI.DomainModels.CreateEntity("Order").
Persistent().
WithDecimalAttribute("TotalAmount").
WithDateTimeAttribute("OrderDate", true).
Build()
// Create an enumeration
_, _ = modelAPI.Enumerations.CreateEnumeration("OrderStatus").
WithValue("Pending", "Pending").
WithValue("Processing", "Processing").
WithValue("Completed", "Completed").
WithValue("Cancelled", "Cancelled").
Build()
// Create association
_, _ = modelAPI.DomainModels.CreateAssociation("Customer_Orders").
From("Customer").
To("Order").
OneToMany().
Build()
Create a Microflow
_, _ = modelAPI.Microflows.CreateMicroflow("ACT_ProcessOrder").
WithParameter("Order", "Sales.Order").
WithStringParameter("Message").
ReturnsBoolean().
Build()
Create a Page
_, _ = modelAPI.Pages.CreatePage("CustomerOverview").
WithTitle("Customer Overview").
Build()
Complete Module Setup
This example creates a full module with entities, enumerations, associations, and microflows:
package main
import (
"log"
"github.com/mendixlabs/mxcli/api"
"github.com/mendixlabs/mxcli/sdk/mpr"
)
func main() {
writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr")
if err != nil {
log.Fatal(err)
}
defer writer.Close()
modelAPI := api.New(writer)
module, _ := modelAPI.Modules.GetModule("ProductCatalog")
modelAPI.SetModule(module)
// Enumeration
_, _ = modelAPI.Enumerations.CreateEnumeration("ProductCategory").
WithValue("Electronics", "Electronics").
WithValue("Clothing", "Clothing").
WithValue("HomeGarden", "Home & Garden").
Build()
// Entities
_, _ = modelAPI.DomainModels.CreateEntity("Product").
Persistent().
WithStringAttribute("SKU", 50).
WithStringAttribute("Name", 200).
WithDecimalAttribute("Price").
WithIntegerAttribute("StockQuantity").
WithBooleanAttribute("IsActive").
WithEnumerationAttribute("Category", "ProductCatalog.ProductCategory").
Build()
_, _ = modelAPI.DomainModels.CreateEntity("Supplier").
Persistent().
WithStringAttribute("CompanyName", 200).
WithStringAttribute("ContactEmail", 254).
Build()
// Association
_, _ = modelAPI.DomainModels.CreateAssociation("Product_Supplier").
From("Product").
To("Supplier").
OneToMany().
Build()
// Microflow
_, _ = modelAPI.Microflows.CreateMicroflow("ValidateProduct").
WithParameter("Product", "ProductCatalog.Product").
ReturnsBoolean().
Build()
}
Comparison: Fluent API vs Direct SDK
The fluent API reduces boilerplate. Here is the same entity creation using both approaches:
Fluent API
entity, _ := modelAPI.DomainModels.CreateEntity("Customer").
Persistent().
WithStringAttribute("Name", 200).
WithIntegerAttribute("Age").
Build()
Direct SDK
import (
"github.com/mendixlabs/mxcli/model"
"github.com/mendixlabs/mxcli/sdk/domainmodel"
modelsdk "github.com/mendixlabs/mxcli"
)
entity := &domainmodel.Entity{
BaseElement: model.BaseElement{
ID: model.ID(modelsdk.GenerateID()),
},
Name: "Customer",
Persistable: true,
Attributes: []*domainmodel.Attribute{
{
BaseElement: model.BaseElement{ID: model.ID(modelsdk.GenerateID())},
Name: "Name",
Type: &domainmodel.StringAttributeType{Length: 200},
},
{
BaseElement: model.BaseElement{ID: model.ID(modelsdk.GenerateID())},
Name: "Age",
Type: &domainmodel.IntegerAttributeType{},
},
},
}
err := writer.CreateEntity(domainModelID, entity)
The fluent API handles ID generation, domain model lookup, and module context automatically.
Connection Statements
Statements for opening and closing Mendix project files.
| Statement | Description |
|---|---|
| OPEN PROJECT | Open a Mendix project file for reading or modification |
| CLOSE PROJECT | Close the currently open project and release the file handle |
OPEN PROJECT
Synopsis
CONNECT LOCAL 'path/to/app.mpr'
Description
Opens a Mendix project file (.mpr) for reading and modification. The path can be absolute or relative to the current working directory. Once connected, all query and mutation statements operate against this project.
MDL supports both MPR v1 (single .mpr SQLite file, Mendix < 10.18) and MPR v2 (.mpr metadata + mprcontents/ folder, Mendix >= 10.18). The format is detected automatically.
Only one project can be open at a time. If a project is already open, close it with DISCONNECT before opening another.
Parameters
- path
- Path to the
.mprfile. Can be an absolute path or a path relative to the current working directory. The path must be enclosed in single quotes.
Examples
Open a project with an absolute path
CONNECT LOCAL '/Users/dev/projects/MyApp/MyApp.mpr';
Open a project with a relative path
CONNECT LOCAL './mx-test-projects/test1-go-app/test1-go.mpr';
Open via the CLI flag
Instead of using CONNECT LOCAL inside the REPL, you can specify the project path when launching the CLI:
-- These are shell commands, not MDL:
-- mxcli -p /path/to/app.mpr
-- mxcli -p app.mpr -c "SHOW ENTITIES"
See Also
CLOSE PROJECT
Synopsis
DISCONNECT
Description
Closes the currently open project and releases the file handle. Any uncommitted changes are discarded. After disconnecting, query and mutation statements are unavailable until a new project is opened with CONNECT LOCAL.
Parameters
This statement takes no parameters.
Examples
Close the current project
DISCONNECT;
Open a different project
DISCONNECT;
CONNECT LOCAL '/path/to/other-project.mpr';
See Also
Query Statements
Statements for browsing and inspecting project elements. Query statements are read-only and never modify the project.
SHOW Statements
| Statement | Description |
|---|---|
| SHOW MODULES | List all modules in the project |
| SHOW ENTITIES | List entities, optionally filtered by module |
| SHOW ASSOCIATIONS | List associations, optionally filtered by module |
| SHOW ENUMERATIONS | List enumerations, optionally filtered by module |
| SHOW MICROFLOWS | List microflows or nanoflows |
| SHOW PAGES | List pages or snippets |
| SHOW CONSTANTS | List constants, optionally filtered by module |
| SHOW WORKFLOWS | List workflows, optionally filtered by module |
| SHOW BUSINESS EVENTS | List business event services |
| SHOW STRUCTURE | Hierarchical project overview at configurable depth |
| SHOW WIDGETS | List widgets across pages with optional filtering |
DESCRIBE Statements
| Statement | Description |
|---|---|
| DESCRIBE ENTITY | Show complete MDL source for an entity |
| DESCRIBE ASSOCIATION | Show association details |
| DESCRIBE ENUMERATION | Show enumeration values and documentation |
| DESCRIBE MICROFLOW | Show complete MDL source for a microflow or nanoflow |
| DESCRIBE PAGE | Show complete MDL source for a page or snippet |
Search
| Statement | Description |
|---|---|
| SEARCH | Full-text search across all project strings and source |
SHOW MODULES
Synopsis
SHOW MODULES
Description
Lists all modules in the current project with their names. This is typically the first command used after connecting to a project to understand its structure.
Module names returned by this command are used as the IN <module> filter for other SHOW and DESCRIBE statements.
Parameters
This statement takes no parameters.
Examples
List all modules in the project:
SHOW MODULES
Use the result to explore a specific module:
SHOW MODULES
SHOW ENTITIES IN MyFirstModule
See Also
SHOW STRUCTURE, SHOW ENTITIES, SHOW MICROFLOWS, SHOW PAGES
SHOW ENTITIES
Synopsis
SHOW ENTITIES [IN <module>]
SHOW ENTITY <qualified_name>
Description
Lists entities in the project. Without the IN clause, lists all entities across all modules. With IN <module>, restricts the listing to entities in the specified module.
The SHOW ENTITY variant displays a summary of a single entity by its qualified name, including its type (persistent/non-persistent) and attribute count.
Parameters
- module
- The name of the module to filter by. Only entities belonging to this module are shown.
- qualified_name
- A
Module.EntityNamereference identifying a specific entity.
Examples
List all entities in the project:
SHOW ENTITIES
List entities in a specific module:
SHOW ENTITIES IN Sales
Show summary of a single entity:
SHOW ENTITY Sales.Customer
See Also
DESCRIBE ENTITY, SHOW ASSOCIATIONS, SHOW MODULES
SHOW MICROFLOWS / SHOW NANOFLOWS
Synopsis
SHOW MICROFLOWS [IN <module>]
SHOW NANOFLOWS [IN <module>]
Description
Lists microflows or nanoflows in the project. Without the IN clause, lists all microflows (or nanoflows) across all modules. With IN <module>, restricts the listing to the specified module.
Microflows run on the server side and can perform database operations, call external services, and execute Java actions. Nanoflows run on the client side and are used for offline-capable and low-latency logic.
Parameters
- module
- The name of the module to filter by. Only microflows or nanoflows belonging to this module are shown.
Examples
List all microflows in the project:
SHOW MICROFLOWS
List microflows in a specific module:
SHOW MICROFLOWS IN Administration
List all nanoflows:
SHOW NANOFLOWS
List nanoflows in a specific module:
SHOW NANOFLOWS IN MyFirstModule
See Also
DESCRIBE MICROFLOW, SHOW PAGES, SHOW MODULES
SHOW PAGES / SHOW SNIPPETS
Synopsis
SHOW PAGES [IN <module>]
SHOW SNIPPETS [IN <module>]
Description
Lists pages or snippets in the project. Without the IN clause, lists all pages (or snippets) across all modules. With IN <module>, restricts the listing to the specified module.
Pages are the user interface screens of a Mendix application. Snippets are reusable page fragments that can be embedded in multiple pages via SNIPPETCALL widgets.
Parameters
- module
- The name of the module to filter by. Only pages or snippets belonging to this module are shown.
Examples
List all pages in the project:
SHOW PAGES
List pages in a specific module:
SHOW PAGES IN Sales
List all snippets:
SHOW SNIPPETS
List snippets in a specific module:
SHOW SNIPPETS IN Common
See Also
DESCRIBE PAGE, SHOW WIDGETS, SHOW MODULES
SHOW ENUMERATIONS
Synopsis
SHOW ENUMERATIONS [IN <module>]
Description
Lists enumerations in the project. Without the IN clause, lists all enumerations across all modules. With IN <module>, restricts the listing to enumerations in the specified module.
Enumerations define a fixed set of named values that can be used as attribute types on entities.
Parameters
- module
- The name of the module to filter by. Only enumerations belonging to this module are shown.
Examples
List all enumerations in the project:
SHOW ENUMERATIONS
List enumerations in a specific module:
SHOW ENUMERATIONS IN Sales
See Also
DESCRIBE ENUMERATION, SHOW ENTITIES, SHOW MODULES
SHOW ASSOCIATIONS
Synopsis
SHOW ASSOCIATIONS [IN <module>]
SHOW ASSOCIATION <qualified_name>
Description
Lists associations in the project. Without the IN clause, lists all associations across all modules. With IN <module>, restricts the listing to associations in the specified module.
The SHOW ASSOCIATION variant displays a summary of a single association by its qualified name, including the FROM entity, TO entity, and association type.
Parameters
- module
- The name of the module to filter by. Only associations belonging to this module are shown.
- qualified_name
- A
Module.AssociationNamereference identifying a specific association.
Examples
List all associations in the project:
SHOW ASSOCIATIONS
List associations in a specific module:
SHOW ASSOCIATIONS IN Sales
Show a single association:
SHOW ASSOCIATION Sales.Order_Customer
See Also
DESCRIBE ASSOCIATION, SHOW ENTITIES, SHOW MODULES
SHOW CONSTANTS
Synopsis
SHOW CONSTANTS [IN <module>]
Description
Lists constants defined in the project. Without the IN clause, lists all constants across all modules. With IN <module>, restricts the listing to constants in the specified module.
Constants are named values (strings, integers, booleans, etc.) that can be configured per deployment environment. They are typically used for API URLs, feature flags, and configuration values.
Parameters
- module
- The name of the module to filter by. Only constants belonging to this module are shown.
Examples
List all constants in the project:
SHOW CONSTANTS
List constants in a specific module:
SHOW CONSTANTS IN MyModule
See Also
SHOW WORKFLOWS
Synopsis
SHOW WORKFLOWS [IN <module>]
Description
Lists workflows defined in the project. Without the IN clause, lists all workflows across all modules. With IN <module>, restricts the listing to workflows in the specified module.
Workflows model multi-step business processes with user tasks, decisions, parallel paths, and microflow calls.
Parameters
- module
- The name of the module to filter by. Only workflows belonging to this module are shown.
Examples
List all workflows in the project:
SHOW WORKFLOWS
List workflows in a specific module:
SHOW WORKFLOWS IN Approvals
See Also
SHOW MODULES, SHOW MICROFLOWS, SHOW STRUCTURE
SHOW BUSINESS EVENTS
Synopsis
SHOW BUSINESS EVENTS [IN <module>]
Description
Lists business event services defined in the project. Without the IN clause, lists all business event services across all modules. With IN <module>, restricts the listing to services in the specified module.
Business event services enable asynchronous, event-driven communication between Mendix applications using the Mendix Business Events platform.
Parameters
- module
- The name of the module to filter by. Only business event services belonging to this module are shown.
Examples
List all business event services:
SHOW BUSINESS EVENTS
List business event services in a specific module:
SHOW BUSINESS EVENTS IN OrderModule
See Also
SHOW IMAGE COLLECTION
Synopsis
SHOW IMAGE COLLECTION;
SHOW IMAGE COLLECTION IN module;
Description
Lists all image collections in the project. Use IN module to filter to a specific module. For full details including embedded images, use DESCRIBE IMAGE COLLECTION.
Parameters
- IN module
- Optional. Restricts the listing to collections in the specified module.
Examples
List all collections
SHOW IMAGE COLLECTION;
Filter by module
SHOW IMAGE COLLECTION IN MyModule;
Describe a specific collection
DESCRIBE IMAGE COLLECTION MyModule.AppIcons;
See Also
DESCRIBE IMAGE COLLECTION, CREATE IMAGE COLLECTION, SHOW MODULES
SHOW STRUCTURE
Synopsis
SHOW STRUCTURE [DEPTH 1|2|3] [IN <module>] [ALL]
Description
Shows a hierarchical overview of the project structure. The output depth and scope can be controlled with optional clauses. By default, displays depth 2 (documents with signatures) for user modules only, excluding system and marketplace modules.
This is useful for getting a quick birds-eye view of what a project contains without examining each element individually.
Parameters
- DEPTH
- Controls the level of detail in the output.
1shows one line per module with element counts.2(default) shows documents with their signatures.3shows full detail including typed attributes and named parameters. - module
- When
IN <module>is specified, restricts output to a single module. Implicitly uses depth 2 unless overridden. - ALL
- Include system and marketplace modules in the output. Without this flag, only user-created modules are shown.
Examples
Show module-level summary (one line per module):
SHOW STRUCTURE DEPTH 1
Show default structure (documents with signatures, user modules only):
SHOW STRUCTURE
Show structure of a single module:
SHOW STRUCTURE IN MyFirstModule
Show full detail including all modules:
SHOW STRUCTURE DEPTH 3 ALL
See Also
SHOW MODULES, SHOW ENTITIES, SHOW MICROFLOWS, SHOW PAGES
SHOW WIDGETS
Synopsis
SHOW WIDGETS [IN <module>] [WHERE <condition>]
Description
Lists widgets used across pages in the project. This command queries the catalog and requires REFRESH CATALOG FULL to have been run first. Without filters, lists all widgets. The IN clause restricts results to a specific module, and the WHERE clause enables filtering on widget properties.
This is useful for auditing widget usage, finding pages that use a particular widget type, or planning bulk widget updates.
Parameters
- module
- The name of the module to filter by. Only widgets on pages belonging to this module are shown.
- condition
- A filter expression on widget properties. Supported filter columns include
WidgetType,PageName,ModuleName, and other catalog fields.
Examples
List all widgets (requires catalog):
REFRESH CATALOG FULL
SHOW WIDGETS
List widgets in a specific module:
SHOW WIDGETS IN Sales
Filter widgets by type:
SHOW WIDGETS WHERE WidgetType = 'DataGrid'
See Also
SHOW PAGES, DESCRIBE PAGE, SHOW STRUCTURE
DESCRIBE ENTITY
Synopsis
DESCRIBE ENTITY <qualified_name>
Description
Shows the complete MDL source for an entity, including its type (persistent, non-persistent, view, external), all attributes with types and constraints, indexes, access rules, and documentation comments. The output is round-trippable MDL that can be used directly in CREATE ENTITY statements.
This is the primary way to inspect the full definition of an entity before modifying it with ALTER ENTITY or recreating it with CREATE OR MODIFY.
Parameters
- qualified_name
- A
Module.EntityNamereference identifying the entity to describe. Both the module and entity name are required.
Examples
Describe a customer entity:
DESCRIBE ENTITY Sales.Customer
Example output:
/**
* Customer entity stores customer information.
*/
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
/** Customer identifier */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
Name: String(200) NOT NULL,
Email: String(200),
Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name);
Describe a non-persistent entity:
DESCRIBE ENTITY Sales.CustomerFilter
See Also
SHOW ENTITIES, DESCRIBE ASSOCIATION, DESCRIBE ENUMERATION
DESCRIBE MICROFLOW / DESCRIBE NANOFLOW
Synopsis
DESCRIBE MICROFLOW <qualified_name>
DESCRIBE NANOFLOW <qualified_name>
Description
Shows the complete MDL source for a microflow or nanoflow, including parameters, return type, variable declarations, activities, control flow (IF/LOOP), error handling, and annotations. The output is round-trippable MDL that can be used directly in CREATE MICROFLOW or CREATE NANOFLOW statements.
This is useful for understanding existing logic before modifying it, or for extracting a microflow definition to use as a template.
Parameters
- qualified_name
- A
Module.MicroflowNameorModule.NanoflowNamereference identifying the flow to describe. Both the module and flow name are required.
Examples
Describe a microflow:
DESCRIBE MICROFLOW Sales.ACT_CreateOrder
Example output:
CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Status = 'Draft'
);
COMMIT $Order;
SHOW PAGE Sales.Order_Edit ($Order = $Order);
RETURN $Order;
END;
Describe a nanoflow:
DESCRIBE NANOFLOW MyModule.NAV_ValidateInput
See Also
SHOW MICROFLOWS, DESCRIBE PAGE, DESCRIBE ENTITY
DESCRIBE PAGE / DESCRIBE SNIPPET
Synopsis
DESCRIBE PAGE <qualified_name>
DESCRIBE SNIPPET <qualified_name>
Description
Shows the complete MDL source for a page or snippet, including page properties (title, layout, parameters), the full widget tree with all property values, and nested widget structures. The output is round-trippable MDL that can be used directly in CREATE PAGE or CREATE SNIPPET statements.
This is particularly useful before using ALTER PAGE or ALTER SNIPPET, as it reveals the widget names needed for targeted modifications.
Parameters
- qualified_name
- A
Module.PageNameorModule.SnippetNamereference identifying the page or snippet to describe. Both the module and document name are required.
Examples
Describe a page:
DESCRIBE PAGE Sales.Customer_Edit
Example output:
CREATE PAGE Sales.Customer_Edit
(
Params: { $Customer: Sales.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
Describe a snippet:
DESCRIBE SNIPPET Common.NavigationMenu
See Also
SHOW PAGES, SHOW WIDGETS, DESCRIBE MICROFLOW
DESCRIBE ENUMERATION
Synopsis
DESCRIBE ENUMERATION <qualified_name>
Description
Shows the complete MDL source for an enumeration, including all values with their captions and any documentation comments. The output is round-trippable MDL that can be used directly in a CREATE ENUMERATION statement.
Enumerations define a fixed set of named values used as attribute types on entities.
Parameters
- qualified_name
- A
Module.EnumerationNamereference identifying the enumeration to describe. Both the module and enumeration name are required.
Examples
Describe an enumeration:
DESCRIBE ENUMERATION Sales.OrderStatus
Example output:
/** Order status enumeration */
CREATE ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending Approval',
Approved 'Approved',
Shipped 'Shipped',
Delivered 'Delivered',
Cancelled 'Cancelled'
);
See Also
SHOW ENUMERATIONS, DESCRIBE ENTITY, SHOW MODULES
DESCRIBE ASSOCIATION
Synopsis
DESCRIBE ASSOCIATION <qualified_name>
Description
Shows the complete MDL source for an association, including the FROM entity, TO entity, association type (Reference or ReferenceSet), owner, and delete behavior. The output is round-trippable MDL that can be used directly in a CREATE ASSOCIATION statement.
Associations define relationships between entities. A Reference type is a one-to-many relationship, while ReferenceSet is many-to-many.
Parameters
- qualified_name
- A
Module.AssociationNamereference identifying the association to describe. Both the module and association name are required.
Examples
Describe an association:
DESCRIBE ASSOCIATION Sales.Order_Customer
Example output:
/** Links orders to customers */
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;
Describe a many-to-many association:
DESCRIBE ASSOCIATION Sales.Order_Product
See Also
SHOW ASSOCIATIONS, DESCRIBE ENTITY, SHOW MODULES
SEARCH
Synopsis
SEARCH '<keyword>'
Description
Performs a full-text search across all strings, messages, captions, and MDL source in the project. Uses FTS5 (SQLite full-text search) for fast matching. Returns results grouped by document type and location.
This command requires the catalog to be populated. If search returns no results, run REFRESH CATALOG FULL first. The search is also available as a CLI subcommand: mxcli search -p app.mpr "keyword" --format names|json.
Parameters
- keyword
- The search term to look for. Must be enclosed in single quotes. Matches against entity names, attribute names, microflow activity captions, page titles, widget labels, string constants, log messages, and MDL source text.
Examples
Search for a keyword:
SEARCH 'Customer'
Search for an error message:
SEARCH 'is required'
Search using the CLI:
-- From the command line:
-- mxcli search -p app.mpr "Customer" --format names
-- mxcli search -p app.mpr "Customer" --format json
See Also
SHOW STRUCTURE, SHOW ENTITIES, SHOW MICROFLOWS
Domain Model Statements
Statements for creating, altering, and dropping domain model elements: entities, enumerations, associations, and constants.
Entity Statements
| Statement | Description |
|---|---|
| CREATE ENTITY | Create a new entity in a module’s domain model |
| ALTER ENTITY | Add, drop, modify, or rename attributes and indexes on an existing entity |
| DROP ENTITY | Remove an entity from the domain model |
Enumeration Statements
| Statement | Description |
|---|---|
| CREATE ENUMERATION | Create a new enumeration type with named values |
| DROP ENUMERATION | Remove an enumeration from the project |
Association Statements
| Statement | Description |
|---|---|
| CREATE ASSOCIATION | Create a relationship between two entities |
| DROP ASSOCIATION | Remove an association from the domain model |
Constant Statements
| Statement | Description |
|---|---|
| CREATE CONSTANT | Create a named constant value in a module |
CREATE ENTITY
Synopsis
CREATE [ OR MODIFY ] entity_type ENTITY module.name
[ EXTENDS parent_module.parent_entity ]
(
attribute_name : data_type [ NOT NULL [ ERROR 'message' ] ]
[ UNIQUE [ ERROR 'message' ] ]
[ DEFAULT value ]
[ CALCULATED [ BY module.microflow ] ]
[, ...]
)
[ INDEX ( column [ ASC | DESC ] [, ...] ) ]
[, ...]
CREATE VIEW ENTITY module.name (
attribute_name : data_type [, ...]
) AS
oql_query
Where entity_type is one of:
PERSISTENT
NON-PERSISTENT
EXTERNAL
Description
CREATE ENTITY adds a new entity to a module’s domain model. The entity type determines how instances are stored:
- PERSISTENT entities are backed by a database table. This is the most common type and is required for data that must survive a server restart.
- NON-PERSISTENT entities exist only in memory for the duration of a user session. They are commonly used for search filters, wizard state, and transient view models.
- VIEW entities are read-only entities backed by an OQL query. They appear in the domain model but do not have their own database table.
- EXTERNAL entities are sourced from an external service (e.g., OData). See
CREATE EXTERNAL ENTITYfor the full syntax.
If OR MODIFY is specified, the statement is idempotent: if an entity with the same qualified name already exists, its attributes are updated to match the definition. New attributes are added, and existing attributes have their types and constraints updated. This is useful for repeatable scripts.
The optional EXTENDS clause establishes generalization (inheritance). The child entity inherits all attributes from the parent and can add its own. Common parent entities include System.User, System.FileDocument, and System.Image. The EXTENDS clause must appear before the opening parenthesis.
Each attribute definition specifies a name, a data type, and optional constraints. Attributes are separated by commas. The supported data types are:
| Type | Syntax | Notes |
|---|---|---|
| String | String(length) | Length in characters; use String(unlimited) for unbounded |
| Integer | Integer | 32-bit signed integer |
| Long | Long | 64-bit signed integer |
| Decimal | Decimal | Arbitrary-precision decimal |
| Boolean | Boolean | TRUE or FALSE; defaults to FALSE when no DEFAULT is given |
| DateTime | DateTime | Date and time combined |
| Date | Date | Date only |
| AutoNumber | AutoNumber | Auto-incrementing integer (persistent entities only) |
| Binary | Binary | Binary data |
| HashedString | HashedString | One-way hashed string (for passwords) |
| Enumeration | Enumeration(Module.EnumName) | Reference to an enumeration type |
Documentation comments (/** ... */) placed immediately before the entity or an individual attribute are preserved as documentation in the domain model.
The @Position(x, y) annotation controls the entity’s visual position in the domain model diagram in Mendix Studio Pro.
One or more INDEX clauses may follow the attribute list to create database indexes. Each index lists one or more columns with an optional sort direction (ASC or DESC, defaulting to ASC).
Parameters
- OR MODIFY
- Makes the statement idempotent. If the entity already exists, its definition is updated to match. New attributes are added and existing attributes are modified. Without this clause, creating a duplicate entity is an error.
- entity_type
- One of
PERSISTENT,NON-PERSISTENT,VIEW, orEXTERNAL. Determines how entity instances are stored.PERSISTENTis the most common choice. - module.name
- The qualified name of the entity in the form
Module.EntityName. The module must already exist. - EXTENDS parent_module.parent_entity
- Establishes generalization (inheritance). The new entity inherits all attributes from the parent. Must appear before the opening parenthesis.
- attribute_name
- The name of an attribute. Must be a valid identifier and unique within the entity.
- data_type
- The attribute’s data type. See the table above for all supported types.
Stringrequires an explicit length argument. - NOT NULL
- Marks the attribute as required. An optional
ERROR 'message'clause provides a custom validation message shown when the constraint is violated. - UNIQUE
- Marks the attribute as having a uniqueness constraint. An optional
ERROR 'message'clause provides a custom validation message. - DEFAULT value
- Sets the default value assigned to the attribute when a new object is created. String defaults are quoted (
'value'), numeric defaults are bare (0,3.14), and boolean defaults areTRUEorFALSE. - CALCULATED
- Marks the attribute as calculated (not stored in the database). The attribute’s value is derived at runtime. Use
CALCULATED BY Module.Microflowto specify the microflow that computes the value. - INDEX ( column [, …] )
- Creates a database index on the listed columns. Each column may have an optional
ASCorDESCsort direction. MultipleINDEXclauses create multiple indexes.
Examples
Basic persistent entity
CREATE PERSISTENT ENTITY Sales.Product (
Name: String(200) NOT NULL,
Price: Decimal DEFAULT 0,
InStock: Boolean DEFAULT TRUE
);
Entity with all constraint types
/** Customer master data */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
/** Unique customer identifier */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Customer full name */
Name: String(200) NOT NULL ERROR 'Name is required',
Email: String(200) UNIQUE ERROR 'Email must be unique',
Balance: Decimal DEFAULT 0,
IsActive: Boolean DEFAULT TRUE,
CreatedDate: DateTime,
Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name)
INDEX (Email);
Entity with generalization (EXTENDS)
/** Employee extends the system user entity */
CREATE PERSISTENT ENTITY HR.Employee EXTENDS System.User (
EmployeeNumber: String(20) NOT NULL UNIQUE,
Department: String(100),
HireDate: Date
);
File and image entities
CREATE PERSISTENT ENTITY Catalog.ProductPhoto EXTENDS System.Image (
Caption: String(200),
SortOrder: Integer DEFAULT 0
);
CREATE PERSISTENT ENTITY Docs.Attachment EXTENDS System.FileDocument (
Description: String(500)
);
Non-persistent entity (search filter)
CREATE NON-PERSISTENT ENTITY Sales.CustomerFilter (
SearchName: String(200),
MinBalance: Decimal,
MaxBalance: Decimal,
IncludeInactive: Boolean DEFAULT FALSE
);
Idempotent script with OR MODIFY
CREATE OR MODIFY PERSISTENT ENTITY Sales.Customer (
CustomerId: AutoNumber NOT NULL UNIQUE,
Name: String(200) NOT NULL,
Email: String(200),
Phone: String(50)
);
If Sales.Customer already exists, the Phone attribute is added and existing attributes are updated. If it does not exist, it is created.
View entity with OQL query
CREATE VIEW ENTITY Reports.CustomerSummary (
CustomerName: String,
TotalOrders: Integer,
TotalAmount: Decimal
) AS
SELECT
c.Name AS CustomerName,
COUNT(o.OrderId) AS TotalOrders,
SUM(o.Amount) AS TotalAmount
FROM Sales.Customer c
LEFT JOIN Sales.Order o ON o.Customer = c
GROUP BY c.Name;
Entity with calculated attribute
CREATE PERSISTENT ENTITY Sales.Order (
OrderNumber: String(50) NOT NULL UNIQUE,
Subtotal: Decimal DEFAULT 0,
TaxRate: Decimal DEFAULT 0,
TotalAmount: Decimal CALCULATED BY Sales.CalcOrderTotal
);
Entity with composite index
CREATE PERSISTENT ENTITY Sales.Order (
OrderId: AutoNumber NOT NULL UNIQUE,
OrderNumber: String(50) NOT NULL UNIQUE,
CustomerId: Long,
OrderDate: DateTime,
Status: Enumeration(Sales.OrderStatus)
)
INDEX (OrderNumber)
INDEX (CustomerId, OrderDate DESC)
INDEX (Status);
Entity with documentation
/**
* Represents a customer order.
* Orders track purchases from initial draft through delivery.
*/
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Order (
/** Auto-generated order identifier */
OrderId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Human-readable order number */
OrderNumber: String(50) NOT NULL UNIQUE,
OrderDate: DateTime NOT NULL,
TotalAmount: Decimal DEFAULT 0,
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft',
Notes: String(unlimited)
)
INDEX (OrderNumber)
INDEX (OrderDate DESC);
Notes
Stringrequires an explicit length:String(200). UseString(unlimited)for unbounded text.EXTENDSmust appear before the opening parenthesis, not after the closing one.- Boolean attributes without an explicit
DEFAULTautomatically default toFALSE. AutoNumberattributes are only valid on persistent entities.- The
@Positionannotation is optional and only affects the visual layout in Mendix Studio Pro. - Statements can be terminated with
;or/(Oracle-style).
See Also
ALTER ENTITY, DROP ENTITY, CREATE ASSOCIATION, CREATE ENUMERATION
ALTER ENTITY
Synopsis
ALTER ENTITY module.name ADD ( attribute_definition [, ...] )
ALTER ENTITY module.name DROP ( attribute_name [, ...] )
ALTER ENTITY module.name MODIFY ( attribute_definition [, ...] )
ALTER ENTITY module.name RENAME old_name TO new_name
ALTER ENTITY module.name ADD INDEX ( column [ ASC | DESC ] [, ...] )
ALTER ENTITY module.name DROP INDEX ( column [, ...] )
ALTER ENTITY module.name SET DOCUMENTATION 'text'
Description
ALTER ENTITY modifies an existing entity without replacing it entirely. This is useful for incremental changes to a domain model – adding a new attribute, removing an obsolete one, renaming for clarity, or managing indexes.
Each ALTER ENTITY statement performs exactly one operation. To make multiple changes, issue multiple statements.
The ADD operation appends one or more new attributes to the entity. Attribute definitions follow the same syntax as in CREATE ENTITY: a name, a data type, and optional constraints (NOT NULL, UNIQUE, DEFAULT).
The DROP operation removes one or more attributes by name. Dropping an attribute also removes any validation rules and index entries that reference it.
The MODIFY operation changes the type or constraints of existing attributes. The attribute name must already exist in the entity. The full attribute definition (type and constraints) replaces the current one.
The RENAME operation changes an attribute’s name. This updates references within the entity but does not automatically update microflows, pages, or access rules that reference the old name.
The ADD INDEX and DROP INDEX operations manage database indexes. Index columns may include an optional ASC or DESC sort direction.
The SET DOCUMENTATION operation replaces the entity’s documentation string.
Parameters
- module.name
- The qualified name of the entity to modify, in the form
Module.EntityName. - attribute_definition
- An attribute name followed by a colon, data type, and optional constraints. Same syntax as in
CREATE ENTITY. - attribute_name
- The name of an existing attribute to drop.
- old_name
- The current name of the attribute to rename.
- new_name
- The new name for the attribute.
- column
- An attribute name for the index, optionally followed by
ASCorDESC. - text
- A single-quoted string to use as the entity’s documentation.
Examples
Add new attributes
ALTER ENTITY Sales.Customer
ADD (Phone: String(50), Notes: String(unlimited));
Drop an attribute
ALTER ENTITY Sales.Customer
DROP (Notes);
Modify an attribute’s type
ALTER ENTITY Sales.Customer
MODIFY (Phone: String(100) NOT NULL);
Rename an attribute
ALTER ENTITY Sales.Customer
RENAME Phone TO PhoneNumber;
Add an index
ALTER ENTITY Sales.Customer
ADD INDEX (Email);
Add a composite index with sort direction
ALTER ENTITY Sales.Order
ADD INDEX (CustomerId, OrderDate DESC);
Drop an index
ALTER ENTITY Sales.Customer
DROP INDEX (Email);
Set documentation
ALTER ENTITY Sales.Customer
SET DOCUMENTATION 'Customer master data for the sales module.';
Notes
- Each
ALTER ENTITYstatement performs a single operation. Chain multiple statements for multiple changes. RENAMEdoes not update references in microflows, pages, or access rules. Update those separately or useSHOW IMPACT OFto find affected elements.DROPremoves the attribute’s validation rules and index entries automatically.
See Also
DROP ENTITY
Synopsis
DROP ENTITY module.name
Description
DROP ENTITY removes an entity from a module’s domain model. The entity and all of its attributes, validation rules, indexes, and access rules are deleted.
Associations that reference the dropped entity are not automatically removed. Drop those separately with DROP ASSOCIATION before or after dropping the entity to avoid dangling references.
Use SHOW IMPACT OF Module.EntityName to check which microflows, pages, and associations reference the entity before dropping it.
Parameters
- module.name
- The qualified name of the entity to remove, in the form
Module.EntityName.
Examples
Drop a single entity
DROP ENTITY Sales.CustomerFilter;
Drop entity after checking impact
-- Check what references this entity
SHOW IMPACT OF Sales.OldEntity;
-- Drop related association first
DROP ASSOCIATION Sales.OldEntity_Customer;
-- Then drop the entity
DROP ENTITY Sales.OldEntity;
See Also
CREATE ENTITY, ALTER ENTITY, DROP ASSOCIATION
CREATE ENUMERATION
Synopsis
CREATE [ OR MODIFY ] ENUMERATION module.name (
value_name 'caption'
[, ...]
)
Description
CREATE ENUMERATION defines a new enumeration type in a module. An enumeration is a fixed set of named values that can be used as an attribute type on entities (via Enumeration(Module.EnumName)).
Each enumeration value has two parts: a value name (an identifier used in code and expressions) and a caption (a display string shown in the UI). The value name must be a valid identifier. The caption is a single-quoted string.
If OR MODIFY is specified, the statement is idempotent. If the enumeration already exists, its values are updated to match the definition.
A documentation comment (/** ... */) placed before the statement is preserved as the enumeration’s documentation.
Parameters
- OR MODIFY
- Makes the statement idempotent. If the enumeration already exists, its values are replaced with the new definition. Without this clause, creating a duplicate enumeration is an error.
- module.name
- The qualified name of the enumeration in the form
Module.EnumerationName. The module must already exist. - value_name
- An identifier for the enumeration value. Used in expressions and microflow logic.
- caption
- A single-quoted display string for the value. This is what end users see in the UI.
Examples
Basic enumeration
CREATE ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending Approval',
Approved 'Approved',
Shipped 'Shipped',
Delivered 'Delivered',
Cancelled 'Cancelled'
);
Enumeration with documentation
/** Priority levels for support tickets */
CREATE ENUMERATION Support.TicketPriority (
Low 'Low',
Medium 'Medium',
High 'High',
Critical 'Critical'
);
Idempotent with OR MODIFY
CREATE OR MODIFY ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending Approval',
Approved 'Approved',
Shipped 'Shipped',
Delivered 'Delivered',
Returned 'Returned',
Cancelled 'Cancelled'
);
Using an enumeration as an attribute type
CREATE PERSISTENT ENTITY Sales.Order (
OrderNumber: String(50) NOT NULL,
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
);
Notes
- Enumeration values can also be managed incrementally with
ALTER ENUMERATION ... ADD VALUEandALTER ENUMERATION ... REMOVE VALUE. - The
DEFAULTvalue on an entity attribute references the enumeration value name as a quoted string (e.g.,DEFAULT 'Draft').
See Also
DROP ENUMERATION, CREATE ENTITY
DROP ENUMERATION
Synopsis
DROP ENUMERATION module.name
Description
DROP ENUMERATION removes an enumeration type from the project. Any entity attributes that reference the enumeration as their type (via Enumeration(Module.EnumName)) will become invalid after the enumeration is dropped.
Use SHOW IMPACT OF Module.EnumName to check which entities and attributes reference the enumeration before dropping it.
Parameters
- module.name
- The qualified name of the enumeration to remove, in the form
Module.EnumerationName.
Examples
Drop an enumeration
DROP ENUMERATION Sales.OrderStatus;
Drop after checking references
SHOW IMPACT OF Sales.OldStatus;
DROP ENUMERATION Sales.OldStatus;
See Also
CREATE ASSOCIATION
Synopsis
CREATE [ OR MODIFY ] ASSOCIATION module.name
FROM from_module.from_entity
TO to_module.to_entity
TYPE { Reference | ReferenceSet }
[ OWNER { Default | Both | Parent | Child } ]
[ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_CASCADE } ]
Description
CREATE ASSOCIATION defines a relationship between two entities in the domain model.
The FROM entity is the side that owns the foreign key (in database terms). The TO entity is the side being referenced. For a typical one-to-many relationship like “a Customer has many Orders,” the FROM side is the parent entity (Customer) and the TO side is the child entity (Order).
Two association types are supported:
- Reference – a many-to-one relationship. Each object on the TO side references at most one object on the FROM side. Stored as a foreign key column in the TO entity’s database table.
- ReferenceSet – a many-to-many relationship. Objects on both sides can reference multiple objects on the other side. Stored in a separate junction table.
The OWNER clause controls which side of the association can modify the relationship:
| Owner | Description |
|---|---|
Default | The default owner (child/TO side) can set and clear the reference |
Both | Both sides can modify the association |
Parent | Only the FROM side can modify the association |
Child | Only the TO side can modify the association |
The DELETE_BEHAVIOR clause controls what happens when an object on the FROM side is deleted:
| Behavior | Description |
|---|---|
DELETE_BUT_KEEP_REFERENCES | Delete the object and set references to null |
DELETE_CASCADE | Delete the object and all associated objects on the TO side |
If OR MODIFY is specified, the statement is idempotent: if the association already exists, it is updated to match the new definition.
A documentation comment (/** ... */) placed before the statement is preserved as the association’s documentation.
Parameters
- OR MODIFY
- Makes the statement idempotent. If the association already exists, its properties are updated. Without this clause, creating a duplicate association is an error.
- module.name
- The qualified name of the association in the form
Module.AssociationName. By convention, association names follow the patternModule.Child_ParentorModule.EntityA_EntityB. - FROM from_module.from_entity
- The entity on the “from” side of the relationship. This is the entity that owns the foreign key in the database. In a one-to-many relationship, this is typically the “one” (parent) side.
- TO to_module.to_entity
- The entity on the “to” side of the relationship. In a one-to-many relationship, this is typically the “many” (child) side that holds the reference.
- TYPE
- Either
Reference(many-to-one) orReferenceSet(many-to-many). - OWNER
- Which side can modify the association. Defaults to
Defaultif omitted. - DELETE_BEHAVIOR
- What happens to associated objects when a FROM-side object is deleted. If omitted, references are kept (the default Mendix behavior).
Examples
Many-to-one: Order belongs to Customer
CREATE ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DELETE_BUT_KEEP_REFERENCES;
Many-to-many: Order has Products
CREATE ASSOCIATION Sales.Order_Product
FROM Sales.Order
TO Sales.Product
TYPE ReferenceSet
OWNER Both;
Cascade delete: Invoice deleted with Order
/** Invoice must be deleted when its Order is deleted */
CREATE ASSOCIATION Sales.Order_Invoice
FROM Sales.Order
TO Sales.Invoice
TYPE Reference
DELETE_BEHAVIOR DELETE_CASCADE;
Idempotent with OR MODIFY
CREATE OR MODIFY ASSOCIATION Sales.Order_Customer
FROM Sales.Customer
TO Sales.Order
TYPE Reference
OWNER Default;
Cross-module association
CREATE ASSOCIATION HR.Employee_Department
FROM HR.Department
TO HR.Employee
TYPE Reference;
Notes
- The naming convention
Module.Child_Parentreflects that the child (TO) entity holds the reference to the parent (FROM) entity. This can be counterintuitive – theFROMentity is the parent, and theTOentity is the child. - Internally, Mendix stores the FROM entity pointer as
ParentPointerand the TO entity pointer asChildPointer. These BSON field names are inverted relative to what you might expect. - Entity access rules for associations (member access) must only be added to the FROM entity. Adding them to the TO entity triggers validation errors.
See Also
DROP ASSOCIATION, CREATE ENTITY
DROP ASSOCIATION
Synopsis
DROP ASSOCIATION module.name
Description
DROP ASSOCIATION removes an association (relationship) between two entities from the domain model. The entities themselves are not affected – only the relationship is removed.
After dropping an association, any microflows, pages, or access rules that traverse it will become invalid. Use SHOW IMPACT OF Module.AssociationName to check references before dropping.
Parameters
- module.name
- The qualified name of the association to remove, in the form
Module.AssociationName.
Examples
Drop an association
DROP ASSOCIATION Sales.Order_Customer;
Drop after checking impact
SHOW IMPACT OF Sales.OldAssociation;
DROP ASSOCIATION Sales.OldAssociation;
See Also
CREATE ASSOCIATION, DROP ENTITY
CREATE CONSTANT
Synopsis
CREATE [ OR MODIFY ] CONSTANT module.name TYPE data_type [ DEFAULT value ]
Description
CREATE CONSTANT defines a named constant in a module. Constants hold configuration values (API URLs, feature flags, limits) that can differ between environments. In Mendix, constant values can be overridden per runtime configuration without changing the model.
The TYPE clause specifies the constant’s data type. Supported types include String, Integer, Long, Decimal, Boolean, and DateTime.
The optional DEFAULT clause sets the constant’s default value. This is the value used at runtime unless overridden by environment configuration.
If OR MODIFY is specified, the statement is idempotent. If the constant already exists, its type and default value are updated.
Constant values can also be overridden per deployment configuration using ALTER SETTINGS CONSTANT.
Parameters
- OR MODIFY
- Makes the statement idempotent. If the constant already exists, its definition is updated. Without this clause, creating a duplicate constant is an error.
- module.name
- The qualified name of the constant in the form
Module.ConstantName. The module must already exist. - data_type
- The constant’s data type. One of:
String,Integer,Long,Decimal,Boolean,DateTime. - DEFAULT value
- The default value for the constant. String values are single-quoted. Numeric values are bare. Boolean values are
trueorfalse.
Examples
String constant for API URL
CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com';
Integer constant for configuration
CREATE CONSTANT MyModule.MaxRetries TYPE Integer DEFAULT 3;
Boolean feature flag
CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
Idempotent with OR MODIFY
CREATE OR MODIFY CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com/v2';
Constant without a default
CREATE CONSTANT MyModule.DatabasePassword TYPE String;
Override constant per configuration
-- Create the constant
CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com';
-- Override in a specific runtime configuration
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://staging.example.com' IN CONFIGURATION 'Staging';
See Also
CREATE ENTITY, CREATE ENUMERATION
Microflow Statements
Statements for creating and dropping microflows, nanoflows, and Java actions.
Microflows are the primary mechanism for implementing business logic in Mendix applications. They execute on the server and have access to the full range of activities including database operations, integrations, and security-sensitive actions.
Nanoflows share the same syntax as microflows but execute on the client (browser or native mobile). They have a restricted set of available activities.
| Statement | Description |
|---|---|
| CREATE MICROFLOW | Create a microflow with activities and control flow |
| CREATE NANOFLOW | Create a client-side nanoflow |
| CREATE JAVA ACTION | Create a Java action stub with inline code |
| DROP MICROFLOW / NANOFLOW | Remove a microflow or nanoflow |
CREATE MICROFLOW
Synopsis
CREATE [ OR REPLACE ] MICROFLOW module.Name
[ ( DECLARE $param : type [, ...] ) ]
[ RETURN type ]
[ FOLDER 'path' ]
BEGIN
statements
END
Description
Creates a new microflow in the specified module. Microflows are the server-side logic building blocks in Mendix – they execute on the application server and have access to the full set of activities including database operations, external service calls, and security-sensitive actions.
If OR REPLACE is specified and a microflow with the same qualified name already exists, it is replaced. Otherwise, creating a microflow with an existing name is an error.
The microflow body consists of a sequence of statements enclosed in BEGIN ... END. Statements are executed in order, with control flow managed by IF, LOOP, WHILE, and RETURN constructs.
Microflow Parameters
Microflow parameters are declared in parentheses after the name. Each parameter has a name (prefixed with $), a colon, and a type. Parameters become variables available throughout the microflow body.
Return Type
If the microflow returns a value, specify RETURN type after the parameter list. The type can be any primitive type, an entity type, or a list type. Every execution path must end with a RETURN statement when a return type is declared.
Folder Placement
The optional FOLDER clause places the microflow in a subfolder within the module. Nested folders use / as separator. Missing folders are created automatically.
Activities
The following activities are available inside the microflow body.
Variable Declaration and Assignment
DECLARE $Var Type = value;
DECLARE $Entity Module.Entity;
DECLARE $List List of Module.Entity = empty;
SET $Var = expression;
Primitive types: String, Integer, Long, Decimal, Boolean, DateTime. Entity declarations do not use = empty. List declarations require = empty to initialize an empty list.
Object Operations
$Var = CREATE Module.Entity ( Attr1 = value1, Attr2 = value2 );
CHANGE $Entity ( Attr = value );
COMMIT $Entity [ WITH EVENTS ] [ REFRESH ];
DELETE $Entity;
ROLLBACK $Entity [ REFRESH ];
CREATE instantiates a new object with initial attribute values. CHANGE modifies attributes on an existing object. COMMIT persists changes to the database – WITH EVENTS triggers before/after commit event handlers, REFRESH updates client-side state. ROLLBACK reverts uncommitted changes to an object.
Retrieval
-- Database retrieve with optional XPath constraint
RETRIEVE $Var FROM Module.Entity [ WHERE condition ] [ LIMIT n ];
-- Retrieve by association
RETRIEVE $List FROM $Parent/Module.AssocName;
RETRIEVE ... LIMIT 1 returns a single entity. Without LIMIT or with LIMIT greater than 1, it returns a list. Retrieve by association traverses an association from a known object.
Calls
$Result = CALL MICROFLOW Module.Name ( Param = $value );
$Result = CALL NANOFLOW Module.Name ( Param = $value );
$Result = CALL JAVA ACTION Module.Name ( Param = value );
Call another microflow, nanoflow, or Java action. Parameters are passed by name. The result can be assigned to a variable when the callee has a return type. If no return value is needed, omit the $Result = prefix.
UI Actions
SHOW PAGE Module.PageName ( $Param = $value );
CLOSE PAGE;
SHOW PAGE opens a page, passing parameters by name. CLOSE PAGE closes the current page.
Validation and Logging
VALIDATION FEEDBACK $Entity/Attribute MESSAGE 'message';
LOG INFO | WARNING | ERROR [ NODE 'name' ] 'message';
VALIDATION FEEDBACK adds a validation error to a specific attribute on an object. LOG writes to the application log at the specified level, optionally tagged with a log node name.
Execute Database Query
$Result = EXECUTE DATABASE QUERY Module.Connector.QueryName;
Executes a Database Connector query. The three-part name identifies the connector module, connection, and query. Supports DYNAMIC, parameters, and CONNECTION override.
Control Flow
IF condition THEN
statements
[ ELSE
statements ]
END IF;
LOOP $Item IN $List BEGIN
statements
END LOOP;
WHILE condition BEGIN
statements
END WHILE;
RETURN $value;
IF branches on a boolean expression. LOOP iterates over each item in a list. WHILE loops while a condition holds true. RETURN ends execution and returns a value (required when the microflow declares a return type).
Error Handling
-- Suffix on any activity (except EXECUTE DATABASE QUERY)
activity ON ERROR CONTINUE;
activity ON ERROR ROLLBACK;
activity ON ERROR {
handler_statements
};
Error handling is attached as a suffix to an individual activity. ON ERROR CONTINUE suppresses the error and continues. ON ERROR ROLLBACK rolls back the current transaction. ON ERROR { ... } executes custom error-handling logic.
Annotations
Annotations are placed before an activity to control visual appearance in the microflow editor:
@position(x, y) -- Canvas position
@caption 'text' -- Custom caption
@color Green -- Background color
@annotation 'text' -- Visual note attached to next activity
Parameters
module.Name- The qualified name of the microflow (
Module.MicroflowName). The module must already exist. $param : type- A parameter declaration. The name must start with
$. The type can be:- Primitive:
String,Integer,Long,Decimal,Boolean,DateTime - Entity:
Module.EntityName - List:
List of Module.EntityName - Enumeration:
Enumeration(Module.EnumName)
- Primitive:
RETURN type- The return type of the microflow. Same type options as parameters.
FOLDER 'path'- Optional folder path within the module. Nested folders use
/separator (e.g.,'Orders/Processing').
Examples
Simple microflow that creates and commits an object:
CREATE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Status = 'Draft'
);
COMMIT $Order;
SHOW PAGE Sales.Order_Edit ($Order = $Order);
RETURN $Order;
END;
Microflow with parameters and conditional logic:
CREATE MICROFLOW Sales.ACT_ApproveOrder
(DECLARE $Order: Sales.Order)
RETURN Boolean
BEGIN
IF $Order/Status = 'Pending' THEN
CHANGE $Order (Status = 'Approved');
COMMIT $Order WITH EVENTS;
LOG INFO NODE 'OrderProcessing' 'Order approved';
RETURN true;
ELSE
VALIDATION FEEDBACK $Order/Status MESSAGE 'Only pending orders can be approved';
RETURN false;
END IF;
END;
Microflow with a loop over a retrieved list:
CREATE MICROFLOW Sales.ACT_DeactivateExpiredCustomers
RETURN Integer
FOLDER 'Scheduled'
BEGIN
DECLARE $Count Integer = 0;
RETRIEVE $Customers FROM Sales.Customer
WHERE [IsActive = true AND ExpiryDate < [%CurrentDateTime%]];
LOOP $Customer IN $Customers BEGIN
CHANGE $Customer (IsActive = false);
COMMIT $Customer;
SET $Count = $Count + 1;
END LOOP;
LOG INFO NODE 'Maintenance' 'Deactivated expired customers';
RETURN $Count;
END;
Microflow with error handling:
CREATE MICROFLOW Integration.ACT_SyncData
BEGIN
DECLARE $Result Integration.SyncResult;
$Result = CREATE Integration.SyncResult (
StartTime = [%CurrentDateTime%],
Status = 'Running'
);
COMMIT $Result;
$Result = CALL MICROFLOW Integration.SUB_FetchExternalData (
SyncResult = $Result
) ON ERROR {
CHANGE $Result (Status = 'Failed');
COMMIT $Result;
LOG ERROR NODE 'Integration' 'Sync failed';
};
CHANGE $Result (
Status = 'Completed',
EndTime = [%CurrentDateTime%]
);
COMMIT $Result;
END;
Microflow calling a Java action:
CREATE MICROFLOW MyModule.ACT_GenerateReport
(DECLARE $StartDate: DateTime, DECLARE $EndDate: DateTime)
RETURN String
BEGIN
DECLARE $Report String = '';
$Report = CALL JAVA ACTION MyModule.JA_GenerateReport (
StartDate = $StartDate,
EndDate = $EndDate
);
RETURN $Report;
END;
Using OR REPLACE to update an existing microflow:
CREATE OR REPLACE MICROFLOW Sales.ACT_CreateOrder
FOLDER 'Orders'
BEGIN
DECLARE $Order Sales.Order;
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Status = 'Draft',
CreatedBy = '[%CurrentUser%]'
);
COMMIT $Order WITH EVENTS;
RETURN $Order;
END;
See Also
CREATE NANOFLOW, DROP MICROFLOW, CREATE JAVA ACTION, DESCRIBE MICROFLOW, GRANT EXECUTE ON MICROFLOW
CREATE NANOFLOW
Synopsis
CREATE [ OR REPLACE ] NANOFLOW module.Name
[ ( DECLARE $param : type [, ...] ) ]
[ RETURN type ]
[ FOLDER 'path' ]
BEGIN
statements
END
Description
Creates a new nanoflow in the specified module. Nanoflows share the same syntax as microflows but execute on the client (web browser or native mobile app) rather than on the server. This makes them suitable for offline-capable logic, instant UI feedback, and reducing server round-trips.
If OR REPLACE is specified and a nanoflow with the same qualified name already exists, it is replaced.
Restricted Activity Set
Because nanoflows run on the client, several server-side activities are not available:
- No direct database retrieval with XPath (
RETRIEVE ... FROM Entity WHERE ...over database) - No
COMMITwith database transactions - No
LOGstatements (server log is inaccessible) - No
EXECUTE DATABASE QUERY - No server-side Java action calls
Available activities in nanoflows include:
CREATE,CHANGE,DELETE(on client-side objects)COMMIT(triggers synchronization when online)RETRIEVEby associationCALL NANOFLOW(call other nanoflows)CALL MICROFLOW(triggers a server round-trip)SHOW PAGE,CLOSE PAGEVALIDATION FEEDBACKIF,LOOP,WHILE,RETURN- Variable declaration and assignment
Parameters, Return Type, and Folder
These work identically to CREATE MICROFLOW. See that page for full details on types, annotations, and folder placement.
Parameters
module.Name- The qualified name of the nanoflow (
Module.NanoflowName). The module must already exist. $param : type- A parameter declaration. Same types as microflow parameters: primitives, entities, lists, enumerations.
RETURN type- The return type of the nanoflow.
FOLDER 'path'- Optional folder path within the module.
Examples
Nanoflow that validates an object on the client:
CREATE NANOFLOW Sales.NAV_ValidateOrder
(DECLARE $Order: Sales.Order)
RETURN Boolean
BEGIN
IF $Order/CustomerName = empty THEN
VALIDATION FEEDBACK $Order/CustomerName MESSAGE 'Customer name is required';
RETURN false;
END IF;
IF $Order/Amount <= 0 THEN
VALIDATION FEEDBACK $Order/Amount MESSAGE 'Amount must be positive';
RETURN false;
END IF;
RETURN true;
END;
Nanoflow that toggles a UI state:
CREATE NANOFLOW MyModule.NAV_ToggleDetail
(DECLARE $Helper: MyModule.UIHelper)
BEGIN
IF $Helper/ShowDetail = true THEN
CHANGE $Helper (ShowDetail = false);
ELSE
CHANGE $Helper (ShowDetail = true);
END IF;
END;
Nanoflow calling a microflow for server-side work:
CREATE NANOFLOW Sales.NAV_SubmitOrder
(DECLARE $Order: Sales.Order)
BEGIN
CHANGE $Order (Status = 'Submitted');
$Result = CALL MICROFLOW Sales.ACT_ProcessOrder (Order = $Order);
SHOW PAGE Sales.Order_Confirmation ($Order = $Order);
END;
See Also
CREATE MICROFLOW, DROP MICROFLOW, GRANT EXECUTE ON NANOFLOW
DROP MICROFLOW / NANOFLOW
Synopsis
DROP MICROFLOW module.Name
DROP NANOFLOW module.Name
Description
Removes a microflow or nanoflow from the project. The microflow or nanoflow must exist; otherwise an error is raised.
Dropping a microflow or nanoflow does not automatically update references to it from other microflows, pages, navigation, or security rules. Use SHOW IMPACT OF module.Name before dropping to identify all references that will break.
Parameters
module.Name- The qualified name of the microflow or nanoflow to drop (
Module.MicroflowNameorModule.NanoflowName).
Examples
Drop a microflow:
DROP MICROFLOW Sales.ACT_CreateOrder;
Drop a nanoflow:
DROP NANOFLOW Sales.NAV_ValidateOrder;
Check impact before dropping:
-- See what references this microflow
SHOW IMPACT OF Sales.ACT_CreateOrder;
-- Then drop if safe
DROP MICROFLOW Sales.ACT_CreateOrder;
See Also
CREATE MICROFLOW, CREATE NANOFLOW, SHOW IMPACT
CREATE JAVA ACTION
Synopsis
CREATE JAVA ACTION module.Name ( parameters )
RETURNS type
[ EXPOSED AS 'caption' IN 'category' ]
AS $$ java_code $$
DROP JAVA ACTION module.Name
Description
Creates a Java action with inline Java code. Java actions allow custom server-side logic written in Java to be called from microflows.
The action parameters, return type, and Java body are all specified inline. When the action is created, the corresponding Java source file is generated with the parameter boilerplate and the provided code body.
Type Parameters
Java actions support generic entity handling through type parameters. A parameter declared as ENTITY <pEntity> creates a type parameter selector – the caller chooses which entity type to use. Other parameters declared as bare pEntity (without ENTITY) receive instances of the selected entity type.
Exposed Actions
The optional EXPOSED AS clause makes the action visible in the Studio Pro toolbox under the specified category, allowing modelers to use it directly in microflows.
Parameters
module.Name- The qualified name of the Java action.
parameters- Comma-separated parameter declarations. Each parameter has a name, colon, and type. Supported types:
- Primitives:
String,Integer,Long,Decimal,Boolean,DateTime - Entity:
Module.EntityName - List:
List of Module.EntityName - Enumeration:
ENUM Module.EnumNameorEnumeration(Module.EnumName) - String template:
StringTemplate(Sql),StringTemplate(Oql) - Type parameter declaration:
ENTITY <pEntity>(declares the type parameter) - Type parameter reference: bare
pEntity(uses the declared type parameter) - Add
NOT NULLafter the type to mark a parameter as required.
- Primitives:
RETURNS type- The return type. Same type options as parameters.
EXPOSED AS 'caption' IN 'category'- Optional. Makes the action visible in the toolbox with the given caption and category.
AS $$ java_code $$- The Java code body, enclosed in
$$delimiters.
Examples
Simple Java action returning a string:
CREATE JAVA ACTION MyModule.JA_FormatCurrency (
Amount: Decimal NOT NULL,
CurrencyCode: String NOT NULL
) RETURNS String
AS $$
java.text.NumberFormat formatter = java.text.NumberFormat.getCurrencyInstance();
formatter.setCurrency(java.util.Currency.getInstance(CurrencyCode));
return formatter.format(Amount);
$$;
Java action with type parameters for generic entity handling:
CREATE JAVA ACTION MyModule.JA_Validate (
EntityType: ENTITY <pEntity> NOT NULL,
InputObject: pEntity NOT NULL
) RETURNS Boolean
EXPOSED AS 'Validate Entity' IN 'Validation'
AS $$
return InputObject != null;
$$;
Java action with a list parameter:
CREATE JAVA ACTION MyModule.JA_ExportToCsv (
Records: List of MyModule.Customer NOT NULL,
FilePath: String NOT NULL
) RETURNS Boolean
AS $$
// Export logic here
return true;
$$;
Drop a Java action:
DROP JAVA ACTION MyModule.JA_FormatCurrency;
See Also
CREATE MICROFLOW, SHOW JAVA ACTIONS
Page Statements
Statements for creating, modifying, and dropping pages, snippets, and layouts.
Pages define the user interface of a Mendix application. Each page has a layout, a title, optional parameters, and a widget tree that describes the UI structure. Snippets are reusable widget fragments that can be embedded in pages.
| Statement | Description |
|---|---|
| CREATE PAGE | Create a page with a widget tree |
| CREATE SNIPPET | Create a reusable widget fragment |
| ALTER PAGE / ALTER SNIPPET | Modify an existing page or snippet in-place |
| DROP PAGE / SNIPPET | Remove a page or snippet |
| CREATE LAYOUT | Create a page layout |
CREATE PAGE
Synopsis
CREATE [ OR REPLACE ] PAGE module.Name
(
[ Params: { $param : Module.Entity [, ...] }, ]
Title: 'title',
Layout: Module.LayoutName
[, Folder: 'path' ]
[, Variables: { $name : type = 'expression' [, ...] } ]
)
{
widget_tree
}
Description
Creates a new page in the specified module. A page defines a user interface screen with a title, a layout, optional parameters, and a hierarchical widget tree.
If OR REPLACE is specified and a page with the same qualified name already exists, it is replaced. Otherwise, creating a page with an existing name is an error.
Page Properties
The page declaration block ( ... ) contains comma-separated key-value properties:
- Params – Page parameters. Each parameter has a
$-prefixed name and an entity type. Parameters are passed when the page is opened (e.g., from a microflowSHOW PAGEor from a data view). - Title – The page title displayed in the browser tab or page header.
- Layout – The layout the page is based on. Must be a qualified name referring to an existing layout (e.g.,
Atlas_Core.Atlas_Default,Atlas_Core.PopupLayout). - Folder – Optional folder path within the module. Nested folders use
/separator. - Variables – Optional page-level variables with default expressions. Variables can be used for conditional visibility and dynamic behavior.
Widget Tree
The widget tree inside { ... } defines the page content. Widgets are nested hierarchically. Each widget has:
- A type keyword (e.g.,
DATAVIEW,TEXTBOX,ACTIONBUTTON) - A name (unique within the page, used for ALTER PAGE references)
- Properties in parentheses
(Key: value, ...) - Optional children in braces
{ ... }
Widget Types
Layout Widgets
| Widget | Description | Key Properties |
|---|---|---|
LAYOUTGRID | Responsive grid container | Class, Style |
ROW | Grid row | Class |
COLUMN | Grid column | Class |
CONTAINER | Generic div container | Class, Style, DesignProperties |
CUSTOMCONTAINER | Custom container widget | Class, Style |
Data Widgets
| Widget | Description | Key Properties |
|---|---|---|
DATAVIEW | Displays/edits a single object | DataSource |
LISTVIEW | Renders a list of objects | DataSource, PageSize |
DATAGRID | Tabular data display with columns | DataSource, PageSize, Pagination |
GALLERY | Card-based list display | DataSource, PageSize |
Input Widgets
| Widget | Description | Key Properties |
|---|---|---|
TEXTBOX | Single-line text input | Label, Attribute |
TEXTAREA | Multi-line text input | Label, Attribute |
CHECKBOX | Boolean toggle | Label, Attribute |
RADIOBUTTONS | Radio button group | Label, Attribute |
DATEPICKER | Date/time selector | Label, Attribute |
COMBOBOX | Dropdown selector | Label, Attribute |
Display Widgets
| Widget | Description | Key Properties |
|---|---|---|
DYNAMICTEXT | Display-only text bound to an attribute | Attribute |
IMAGE | Generic image | Width, Height |
STATICIMAGE | Fixed image from project resources | Width, Height |
DYNAMICIMAGE | Image from an entity attribute | Width, Height |
Action Widgets
| Widget | Description | Key Properties |
|---|---|---|
ACTIONBUTTON | Button that triggers an action | Caption, Action, ButtonStyle |
LINKBUTTON | Hyperlink-styled action | Caption, Action |
Structure Widgets
| Widget | Description | Key Properties |
|---|---|---|
HEADER | Page header area | – |
FOOTER | Page footer area (typically for buttons) | – |
CONTROLBAR | Control bar for data grids | – |
SNIPPETCALL | Embeds a snippet | Snippet: Module.SnippetName |
NAVIGATIONLIST | Navigation sidebar list | – |
DataSource Types
The DataSource property determines how a data widget obtains its data:
| DataSource | Syntax | Description |
|---|---|---|
| Parameter | DataSource: $ParamName | Binds to a page parameter or enclosing data view |
| Database | DataSource: DATABASE Module.Entity | Retrieves from database |
| Selection | DataSource: SELECTION widgetName | Listens to selection on another widget |
| Microflow | DataSource: MICROFLOW Module.MFName | Calls a microflow for data |
Action Types
The Action property on buttons determines what happens when clicked:
| Action | Syntax | Description |
|---|---|---|
| Save | Action: SAVE_CHANGES | Commits and closes |
| Cancel | Action: CANCEL_CHANGES | Rolls back and closes |
| Microflow | Action: MICROFLOW Module.Name(Param: val) | Calls a microflow |
| Nanoflow | Action: NANOFLOW Module.Name(Param: val) | Calls a nanoflow |
| Page | Action: PAGE Module.PageName | Opens a page |
| Close | Action: CLOSE_PAGE | Closes the current page |
| Delete | Action: DELETE | Deletes the context object |
ButtonStyle Values
Primary, Default, Success, Danger, Warning, Info.
DataGrid Columns
Inside a DATAGRID, child widgets define columns. Each column widget specifies:
| Property | Values | Description |
|---|---|---|
Attribute | attribute name | The entity attribute to display |
Caption | string | Column header (defaults to attribute name) |
Alignment | left, center, right | Text alignment |
WrapText | true, false | Whether text wraps |
Sortable | true, false | Whether column is sortable |
Resizable | true, false | Whether column is resizable |
Draggable | true, false | Whether column is draggable |
Hidable | yes, hidden, no | Column visibility control |
ColumnWidth | autoFill, autoFit, manual | Width mode |
Size | integer (px) | Manual width in pixels |
Visible | expression string | Conditional visibility |
Tooltip | text string | Column tooltip |
Common Widget Properties
These properties are available on most widget types:
| Property | Description | Example |
|---|---|---|
Class | CSS class names | Class: 'card mx-spacing-top-large' |
Style | Inline CSS | Style: 'padding: 16px;' |
Editable | Edit control | Editable: NEVER or Editable: ALWAYS |
Visible | Visibility expression | Visible: '$showField' |
DesignProperties | Atlas design properties | DesignProperties: ['Spacing top': 'Large'] |
Parameters
module.Name- The qualified name of the page (
Module.PageName). The module must already exist. Params: { ... }- Optional page parameters. Each parameter has a
$-prefixed name and an entity type. Title: 'title'- The display title of the page. Required.
Layout: Module.LayoutName- The page layout. Must reference an existing layout. Required.
Folder: 'path'- Optional folder within the module. Nested folders use
/. Variables: { ... }- Optional page variables with type and default expression.
Examples
Edit page with a data view and form fields:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
};
Overview page with a data grid:
CREATE PAGE Sales.Order_Overview
(
Title: 'Orders',
Layout: Atlas_Core.Atlas_Default,
Folder: 'Orders'
)
{
DATAGRID dgOrders (DataSource: DATABASE Sales.Order, PageSize: 20) {
COLUMN colId (Attribute: OrderId, Caption: 'Order #')
COLUMN colDate (Attribute: OrderDate, Caption: 'Date')
COLUMN colStatus (Attribute: Status, Caption: 'Status')
COLUMN colAmount (Attribute: TotalAmount, Caption: 'Amount', Alignment: right)
CONTROLBAR cb1 {
ACTIONBUTTON btnNew (Caption: 'New Order', Action: PAGE Sales.Order_Edit, ButtonStyle: Primary)
}
}
};
Page with layout grid and containers:
CREATE PAGE MyModule.Dashboard
(
Title: 'Dashboard',
Layout: Atlas_Core.Atlas_Default
)
{
LAYOUTGRID lgMain {
ROW row1 {
COLUMN col1 (Class: 'col-md-8') {
CONTAINER cntRecent (Class: 'card') {
LISTVIEW lvRecent (DataSource: DATABASE MyModule.RecentActivity, PageSize: 5) {
DYNAMICTEXT txtDesc (Attribute: Description)
}
}
}
COLUMN col2 (Class: 'col-md-4') {
CONTAINER cntStats (Class: 'card') {
DYNAMICTEXT txtCount (Attribute: TotalCount)
}
}
}
}
};
Page with snippet call:
CREATE PAGE MyModule.Customer_Detail
(
Params: { $Customer: MyModule.Customer },
Title: 'Customer Detail',
Layout: Atlas_Core.Atlas_Default
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
SNIPPETCALL snpHeader (Snippet: MyModule.CustomerHeader)
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
}
};
Page with a gallery and selection-driven detail:
CREATE PAGE Sales.Product_Gallery
(
Title: 'Products',
Layout: Atlas_Core.Atlas_Default
)
{
LAYOUTGRID lgMain {
ROW row1 {
COLUMN col1 (Class: 'col-md-6') {
GALLERY galProducts (DataSource: DATABASE Sales.Product, PageSize: 12) {
DYNAMICTEXT txtName (Attribute: Name)
DYNAMICTEXT txtPrice (Attribute: Price)
}
}
COLUMN col2 (Class: 'col-md-6') {
DATAVIEW dvDetail (DataSource: SELECTION galProducts) {
TEXTBOX txtName (Label: 'Product', Attribute: Name)
TEXTBOX txtDesc (Label: 'Description', Attribute: Description)
}
}
}
}
};
Page with page variables and conditional visibility:
CREATE PAGE MyModule.AdvancedForm
(
Params: { $Item: MyModule.Item },
Title: 'Advanced Form',
Layout: Atlas_Core.Atlas_Default,
Variables: { $showAdvanced: Boolean = 'false' }
)
{
DATAVIEW dvItem (DataSource: $Item) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
ACTIONBUTTON btnToggle (Caption: 'Show Advanced', Action: NANOFLOW MyModule.NAV_Toggle)
CONTAINER cntAdvanced (Visible: '$showAdvanced') {
TEXTAREA taNotes (Label: 'Notes', Attribute: Notes)
}
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
};
Page with microflow data source:
CREATE PAGE Reports.MonthlySummary
(
Title: 'Monthly Summary',
Layout: Atlas_Core.Atlas_Default
)
{
DATAVIEW dvReport (DataSource: MICROFLOW Reports.DS_GetMonthlySummary) {
DYNAMICTEXT txtPeriod (Attribute: Period)
DYNAMICTEXT txtRevenue (Attribute: TotalRevenue)
DYNAMICTEXT txtOrders (Attribute: OrderCount)
}
};
See Also
CREATE SNIPPET, ALTER PAGE, DROP PAGE, DESCRIBE PAGE, GRANT VIEW ON PAGE
CREATE SNIPPET
Synopsis
CREATE [ OR REPLACE ] SNIPPET module.Name
(
[ Params: { $param : Module.Entity [, ...] } ]
[, Folder: 'path' ]
)
{
widget_tree
}
Description
Creates a reusable widget fragment (snippet) in the specified module. Snippets contain a widget tree that can be embedded in pages or other snippets using the SNIPPETCALL widget.
If OR REPLACE is specified and a snippet with the same qualified name already exists, it is replaced.
Snippets support the same widget types and syntax as pages. The key difference is that snippets do not have a Title or Layout – they are fragments meant to be included within a page’s layout.
Snippet Parameters
Snippets can optionally declare parameters. When a snippet has parameters, the SNIPPETCALL widget that embeds it must provide the corresponding values.
Folder Placement
The optional Folder property places the snippet in a subfolder within the module.
Parameters
module.Name- The qualified name of the snippet (
Module.SnippetName). The module must already exist. Params: { ... }- Optional snippet parameters. Each parameter has a
$-prefixed name and an entity type. Folder: 'path'- Optional folder path within the module.
Examples
Simple snippet with a header:
CREATE SNIPPET MyModule.CustomerHeader
(
Params: { $Customer: MyModule.Customer }
)
{
CONTAINER cntHeader (Class: 'card-header') {
DYNAMICTEXT txtName (Attribute: Name)
DYNAMICTEXT txtEmail (Attribute: Email)
}
};
Snippet with form fields:
CREATE SNIPPET MyModule.AddressFields
(
Params: { $Address: MyModule.Address }
)
{
TEXTBOX txtStreet (Label: 'Street', Attribute: Street)
TEXTBOX txtCity (Label: 'City', Attribute: City)
TEXTBOX txtZip (Label: 'Zip Code', Attribute: ZipCode)
TEXTBOX txtCountry (Label: 'Country', Attribute: Country)
};
Snippet without parameters:
CREATE SNIPPET MyModule.AppFooter
{
CONTAINER cntFooter (Class: 'app-footer') {
DYNAMICTEXT txtVersion (Attribute: Version)
}
};
Embedding a snippet in a page:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
SNIPPETCALL snpAddress (Snippet: MyModule.AddressFields)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
}
}
};
See Also
CREATE PAGE, ALTER PAGE, DROP PAGE
ALTER PAGE / ALTER SNIPPET
Synopsis
ALTER PAGE module.Name {
operations
}
ALTER SNIPPET module.Name {
operations
}
Where each operation is one of:
-- Set a property on a widget
SET property = value ON widgetName;
SET ( property1 = value1, property2 = value2 ) ON widgetName;
-- Set a page-level property (no ON clause)
SET Title = 'New Title';
-- Set a pluggable widget property (quoted name)
SET 'propertyName' = value ON widgetName;
-- Insert widgets before or after a target
INSERT BEFORE widgetName { widget_definitions };
INSERT AFTER widgetName { widget_definitions };
-- Remove widgets
DROP WIDGET widgetName1, widgetName2;
-- Replace a widget with new widgets
REPLACE widgetName WITH { widget_definitions };
-- Add a page variable
ADD Variables $name : type = 'expression';
-- Drop a page variable
DROP Variables $name;
Description
Modifies an existing page or snippet in-place without requiring a full CREATE OR REPLACE. This is useful when you need to make targeted changes to a page while preserving the rest of its widget tree, including any unsupported or third-party widget types that cannot be round-tripped through MDL.
ALTER PAGE works directly on the raw BSON widget tree, so it preserves widgets and properties that MDL does not explicitly support.
SET
Sets one or more properties on a named widget. Widget names are assigned during CREATE PAGE and can be discovered with DESCRIBE PAGE.
Supported standard properties: Caption, Label, ButtonStyle, Class, Style, Editable, Visible, Name.
For page-level properties (like Title), omit the ON clause.
For pluggable widget properties, use quoted property names (e.g., 'showLabel').
INSERT BEFORE / INSERT AFTER
Inserts new widgets immediately before or after a named widget within its parent container. The new widgets use the same syntax as in CREATE PAGE.
DROP WIDGET
Removes one or more widgets by name. The widget and all its children are removed from the tree.
REPLACE
Replaces a widget (and its entire subtree) with one or more new widgets.
ADD Variables / DROP Variables
Adds or removes page-level variables. Page variables are typed values with default expressions, available for conditional visibility and dynamic behavior.
Parameters
module.Name- The qualified name of the page or snippet to modify. Must already exist.
widgetName- The name of a widget within the page. Use
DESCRIBE PAGEto list widget names. property- A widget property name. Standard properties are unquoted. Pluggable widget properties use single quotes.
value- The new property value. Strings are quoted. Booleans and enumerations are unquoted.
Examples
Change button caption and style:
ALTER PAGE Sales.Order_Edit {
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;
};
Remove an unused widget and add a new field:
ALTER PAGE Sales.Order_Edit {
DROP WIDGET txtUnused;
INSERT AFTER txtEmail {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
}
};
Replace a widget with multiple new widgets:
ALTER PAGE Sales.Order_Edit {
REPLACE txtOldField WITH {
TEXTBOX txtFirstName (Label: 'First Name', Attribute: FirstName)
TEXTBOX txtLastName (Label: 'Last Name', Attribute: LastName)
}
};
Set a page-level property:
ALTER PAGE Sales.Order_Edit {
SET Title = 'Edit Order Details';
};
Set a pluggable widget property:
ALTER PAGE Sales.Order_Edit {
SET 'showLabel' = false ON cbStatus;
};
Add and drop page variables:
ALTER PAGE Sales.Order_Edit {
ADD Variables $showAdvanced : Boolean = 'false';
};
ALTER PAGE Sales.Order_Edit {
DROP Variables $showAdvanced;
};
Modify a snippet:
ALTER SNIPPET MyModule.NavMenu {
SET Caption = 'Dashboard' ON btnHome;
INSERT AFTER btnHome {
ACTIONBUTTON btnReports (Caption: 'Reports', Action: PAGE MyModule.Reports)
}
};
Combined operations in a single ALTER:
ALTER PAGE MyModule.Customer_Edit {
SET Title = 'Customer Details';
SET (Caption = 'Update', ButtonStyle = Primary) ON btnSave;
DROP WIDGET txtObsolete;
INSERT BEFORE txtEmail {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
}
INSERT AFTER txtEmail {
COMBOBOX cbCategory (Label: 'Category', Attribute: Category)
}
};
See Also
CREATE PAGE, CREATE SNIPPET, DESCRIBE PAGE, DROP PAGE
DROP PAGE / SNIPPET
Synopsis
DROP PAGE module.Name
DROP SNIPPET module.Name
Description
Removes a page or snippet from the project. The page or snippet must exist; otherwise an error is raised.
Dropping a page or snippet does not automatically update references to it from microflows (e.g., SHOW PAGE), navigation menus, other pages (SNIPPETCALL), or security rules. Use SHOW IMPACT OF module.Name before dropping to identify all references that will break.
Parameters
module.Name- The qualified name of the page or snippet to drop (
Module.PageNameorModule.SnippetName).
Examples
Drop a page:
DROP PAGE Sales.Order_Edit;
Drop a snippet:
DROP SNIPPET MyModule.CustomerHeader;
Check impact before dropping:
-- See what references this page
SHOW IMPACT OF Sales.Order_Edit;
-- Then drop if safe
DROP PAGE Sales.Order_Edit;
See Also
CREATE PAGE, CREATE SNIPPET, ALTER PAGE, SHOW IMPACT
CREATE LAYOUT
Synopsis
CREATE LAYOUT module.Name
{
widget_tree
}
Description
Creates a page layout in the specified module. Layouts define the overall structure of pages – they typically include a header, navigation, content placeholder, and footer. Pages reference a layout via the Layout property.
Layout creation in MDL has limited support. Most Mendix projects use layouts provided by the Atlas UI module (e.g., Atlas_Core.Atlas_Default, Atlas_Core.PopupLayout) rather than creating custom layouts through MDL. For advanced layout customization, use Mendix Studio Pro.
Common Atlas Layouts
These layouts are available in most Mendix projects that include Atlas Core:
| Layout | Description |
|---|---|
Atlas_Core.Atlas_Default | Standard responsive page with sidebar navigation |
Atlas_Core.Atlas_TopBar | Page with top navigation bar |
Atlas_Core.PopupLayout | Modal popup dialog |
Atlas_Core.Atlas_Default_NativePhone | Native mobile layout |
Parameters
module.Name- The qualified name of the layout (
Module.LayoutName).
Examples
Reference an existing layout when creating a page:
CREATE PAGE MyModule.Dashboard
(
Title: 'Dashboard',
Layout: Atlas_Core.Atlas_Default
)
{
CONTAINER cntMain {
DYNAMICTEXT txtWelcome (Attribute: WelcomeMessage)
}
};
Reference a popup layout for a dialog page:
CREATE PAGE MyModule.ConfirmDelete
(
Params: { $Item: MyModule.Item },
Title: 'Confirm Delete',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvItem (DataSource: $Item) {
DYNAMICTEXT txtMessage (Attribute: Name)
FOOTER footer1 {
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
};
See Also
Security Statements
Statements for managing project security: module roles, user roles, entity access rules, microflow and page access, demo users, and project-level security settings.
Mendix security operates at two levels. Module roles define permissions within a single module (entity access, microflow execution, page visibility). User roles aggregate module roles into project-wide identities assigned to end users.
Statements
| Statement | Description |
|---|---|
| CREATE MODULE ROLE | Create a role within a module |
| CREATE USER ROLE | Create a project-level user role aggregating module roles |
| GRANT | Grant entity, microflow, page, or nanoflow access to roles |
| REVOKE | Remove previously granted access |
| CREATE DEMO USER | Create a demo user for development and testing |
Related Statements
| Statement | Syntax |
|---|---|
| Show project security | SHOW PROJECT SECURITY |
| Show module roles | SHOW MODULE ROLES [IN module] |
| Show user roles | SHOW USER ROLES |
| Show demo users | SHOW DEMO USERS |
| Show access on element | SHOW ACCESS ON MICROFLOW|PAGE module.Name |
| Show security matrix | SHOW SECURITY MATRIX [IN module] |
| Alter project security level | ALTER PROJECT SECURITY LEVEL OFF|PROTOTYPE|PRODUCTION |
| Toggle demo users | ALTER PROJECT SECURITY DEMO USERS ON|OFF |
| Drop module role | DROP MODULE ROLE module.Role |
| Drop user role | DROP USER ROLE Name |
| Drop demo user | DROP DEMO USER 'username' |
| Alter user role | ALTER USER ROLE Name ADD|REMOVE MODULE ROLES (module.Role, ...) |
CREATE MODULE ROLE
Synopsis
CREATE MODULE ROLE module.RoleName [ DESCRIPTION 'text' ]
Description
Creates a new module role within a module. Module roles are the foundation of Mendix security – they are referenced by entity access rules, microflow/page access grants, and aggregated into project-level user roles.
Each module role belongs to exactly one module. The role name must be unique within the module. By convention, common role names include User, Admin, Viewer, and Manager.
If the module does not exist, the statement returns an error.
Parameters
module.RoleName- Qualified name consisting of the module name and the new role name, separated by a dot. The module must already exist.
DESCRIPTION 'text'- Optional human-readable description of the role’s purpose.
Examples
Create a basic module role:
CREATE MODULE ROLE Shop.Admin;
Create a role with a description:
CREATE MODULE ROLE Shop.Admin DESCRIPTION 'Full administrative access to shop module';
Create multiple roles for a module:
CREATE MODULE ROLE HR.Manager DESCRIPTION 'Can manage employees';
CREATE MODULE ROLE HR.Employee DESCRIPTION 'Self-service access';
CREATE MODULE ROLE HR.Viewer DESCRIPTION 'Read-only access';
See Also
CREATE USER ROLE, GRANT, REVOKE
CREATE USER ROLE
Synopsis
CREATE USER ROLE Name ( module.Role [, ...] ) [ MANAGE ALL ROLES ]
Description
Creates a project-level user role that aggregates one or more module roles. User roles are assigned to end users (either directly or via demo users) and determine the combined set of permissions across all modules.
A user role name must be unique at the project level. It is not qualified with a module name because user roles span multiple modules.
The optional MANAGE ALL ROLES clause grants the user role the ability to manage all other user roles in the application. This is typically reserved for administrator roles.
Parameters
Name- The name of the user role. Not module-qualified. Must be unique among all user roles in the project.
module.Role [, ...]- A comma-separated list of module roles to include. Each module role is specified as a qualified name (
Module.RoleName). The module roles must already exist. MANAGE ALL ROLES- Optional. When specified, users with this role can manage (assign/unassign) all user roles in the application.
Examples
Create an administrator role with management privileges:
CREATE USER ROLE AppAdmin (Shop.Admin, System.Administrator) MANAGE ALL ROLES;
Create a regular user role:
CREATE USER ROLE AppUser (Shop.User, Notifications.Viewer);
Create a role that spans multiple modules:
CREATE USER ROLE SalesManager (
Sales.Manager,
Inventory.Viewer,
Reports.User,
Administration.User
);
See Also
CREATE MODULE ROLE, GRANT, CREATE DEMO USER
GRANT
Synopsis
-- Entity access
GRANT module.Role ON module.Entity ( rights ) [ WHERE 'xpath' ]
-- Microflow access
GRANT EXECUTE ON MICROFLOW module.Name TO module.Role [, ...]
-- Page access
GRANT VIEW ON PAGE module.Name TO module.Role [, ...]
-- Nanoflow access
GRANT EXECUTE ON NANOFLOW module.Name TO module.Role [, ...]
Description
Grants access rights to module roles. There are four forms of the GRANT statement, each controlling a different kind of access.
Entity Access
The entity access form creates an access rule on an entity for a given module role. The rule specifies which CRUD operations are permitted and optionally restricts visibility with an XPath constraint.
Entity access rules control:
- CREATE – whether the role can create new instances
- DELETE – whether the role can delete instances
- READ – which attributes the role can read
- WRITE – which attributes the role can modify
For READ and WRITE, use * to include all members (attributes and associations), or specify a parenthesized list of specific attribute names.
The optional WHERE clause accepts an XPath expression that restricts which objects the role can access. The XPath is enclosed in single quotes. Use doubled single quotes to escape single quotes inside the expression.
Microflow Access
The microflow access form grants execute permission on a microflow to one or more module roles. This controls whether the role can trigger the microflow (from pages, other microflows, or REST/web service calls).
Page Access
The page access form grants view permission on a page to one or more module roles. This controls whether the role can open the page.
Nanoflow Access
The nanoflow access form grants execute permission on a nanoflow to one or more module roles.
Parameters
module.Role- The module role receiving the access grant. Must be a qualified name (
Module.RoleName). module.Entity- The target entity for entity access rules.
rights- A comma-separated list of access rights for entity access. Valid values:
CREATE– allow creating instancesDELETE– allow deleting instancesREAD *– read all membersREAD (Attr1, Attr2, ...)– read specific attributesWRITE *– write all membersWRITE (Attr1, Attr2, ...)– write specific attributes
WHERE 'xpath'- Optional XPath constraint for entity access. Restricts which objects the rule applies to.
module.Name- The target microflow, nanoflow, or page.
TO module.Role [, ...]- One or more module roles receiving the access (for microflow, page, and nanoflow forms).
Examples
Grant full entity access:
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);
Grant read-only access:
GRANT Shop.Viewer ON Shop.Customer (READ *);
Grant selective attribute access:
GRANT Shop.User ON Shop.Customer (READ (Name, Email), WRITE (Email));
Grant entity access with an XPath constraint:
GRANT Shop.User ON Shop.Order (READ *, WRITE *)
WHERE '[Status = ''Open'']';
Grant microflow execution to multiple roles:
GRANT EXECUTE ON MICROFLOW Shop.ACT_Order_Process TO Shop.User, Shop.Admin;
Grant page visibility:
GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;
Grant nanoflow execution:
GRANT EXECUTE ON NANOFLOW Shop.NAV_ValidateInput TO Shop.User;
See Also
REVOKE, CREATE MODULE ROLE, CREATE USER ROLE
REVOKE
Synopsis
-- Entity access
REVOKE module.Role ON module.Entity
-- Microflow access
REVOKE EXECUTE ON MICROFLOW module.Name FROM module.Role [, ...]
-- Page access
REVOKE VIEW ON PAGE module.Name FROM module.Role [, ...]
-- Nanoflow access
REVOKE EXECUTE ON NANOFLOW module.Name FROM module.Role [, ...]
Description
Removes previously granted access rights from module roles. Each form is the counterpart to the corresponding GRANT statement.
Entity Access
Removes the entire entity access rule for the specified module role on the entity. Unlike GRANT, there is no way to partially revoke (e.g., remove only WRITE while keeping READ). The entire rule is removed.
Microflow Access
Removes execute permission on a microflow from one or more module roles.
Page Access
Removes view permission on a page from one or more module roles.
Nanoflow Access
Removes execute permission on a nanoflow from one or more module roles.
Parameters
module.Role- The module role losing access. Must be a qualified name (
Module.RoleName). module.Entity- The entity whose access rule is removed.
module.Name- The target microflow, nanoflow, or page.
FROM module.Role [, ...]- One or more module roles losing access (for microflow, page, and nanoflow forms).
Examples
Remove entity access for a role:
REVOKE Shop.Viewer ON Shop.Customer;
Remove microflow execution from multiple roles:
REVOKE EXECUTE ON MICROFLOW Shop.ACT_Order_Process FROM Shop.Viewer;
Remove page visibility:
REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;
Remove nanoflow execution:
REVOKE EXECUTE ON NANOFLOW Shop.NAV_ValidateInput FROM Shop.Viewer;
See Also
CREATE DEMO USER
Synopsis
CREATE DEMO USER 'username' PASSWORD 'password'
[ ENTITY module.Entity ]
( UserRole [, ...] )
Description
Creates a demo user for development and testing. Demo users appear on the login screen when running the application locally, allowing quick login without manual credential entry.
Demo users require that project security has demo users enabled (ALTER PROJECT SECURITY DEMO USERS ON).
The optional ENTITY clause specifies which entity (a specialization of System.User) stores the demo user. If omitted, the system auto-detects the unique System.User subtype in the project (typically Administration.Account).
Parameters
'username'- The login name for the demo user. Enclosed in single quotes.
PASSWORD 'password'- The password for the demo user. Enclosed in single quotes.
ENTITY module.Entity- Optional. The entity that generalizes
System.User(e.g.,Administration.Account). If the project has exactly oneSystem.Usersubtype, this can be omitted and it will be auto-detected. UserRole [, ...]- One or more project-level user role names (unqualified) to assign to the demo user.
Examples
Create a demo user with auto-detected entity:
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!' (AppAdmin);
Create a demo user with an explicit entity:
CREATE DEMO USER 'demo_admin' PASSWORD 'Admin123!'
ENTITY Administration.Account (AppAdmin);
Create multiple demo users for different roles:
CREATE DEMO USER 'admin' PASSWORD '1' ENTITY Administration.Account (AppAdmin);
CREATE DEMO USER 'user' PASSWORD '1' ENTITY Administration.Account (AppUser);
CREATE DEMO USER 'viewer' PASSWORD '1' ENTITY Administration.Account (AppViewer);
See Also
Navigation Statements
Statements for configuring application navigation profiles, home pages, login pages, and menu structures.
Mendix applications can have multiple navigation profiles (Responsive, Tablet, Phone, NativePhone) that each define a home page, optional login page, optional 404 page, and a hierarchical menu tree. Navigation profiles control which page the user sees first and how they navigate through the application.
Statements
| Statement | Description |
|---|---|
| ALTER NAVIGATION | Create or replace a navigation profile with home pages, login page, and menus |
| SHOW NAVIGATION | Display navigation profiles, menus, and home page assignments |
Related Statements
| Statement | Syntax |
|---|---|
| Describe navigation | DESCRIBE NAVIGATION [profile] |
ALTER NAVIGATION
Synopsis
CREATE OR REPLACE NAVIGATION profile
HOME PAGE module.PageName
[ HOME PAGE module.PageName FOR module.UserRole ]
[ LOGIN PAGE module.PageName ]
[ NOT FOUND PAGE module.PageName ]
[ MENU (
menu_items
) ]
Description
Creates or replaces a navigation profile. Each profile defines the home page, optional role-specific home pages, optional login page, optional 404 page, and a hierarchical menu structure.
The statement fully replaces the existing profile configuration. To modify only part of a profile’s navigation, use DESCRIBE NAVIGATION to export the current configuration, edit the MDL, and re-execute.
Profile Types
Mendix supports the following navigation profile types:
| Profile | Description |
|---|---|
Responsive | Web browser (desktop and mobile responsive) |
Tablet | Tablet-optimized web |
Phone | Phone-optimized web |
NativePhone | Native mobile application |
Menu Items
Menu items form a hierarchy. Top-level items appear in the main navigation bar. Nested submenus are created with the MENU 'label' ( ... ) syntax.
Each MENU ITEM specifies a label and a target page. Menu items are terminated with semicolons.
Parameters
profile- The navigation profile type:
Responsive,Tablet,Phone, orNativePhone. HOME PAGE module.PageName- The default home page for the profile. Required. The page must already exist.
HOME PAGE module.PageName FOR module.UserRole- Optional role-specific home page. Users with this role see a different home page than the default. Multiple role-specific home pages can be specified.
LOGIN PAGE module.PageName- Optional custom login page. If omitted, the default system login page is used.
NOT FOUND PAGE module.PageName- Optional custom 404 page shown when a requested page is not found.
- Optional menu structure. Contains
MENU ITEMand nestedMENUentries. - A leaf menu item that navigates to a page.
- A submenu containing nested menu items and/or further submenus.
Examples
Minimal navigation with just a home page:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web;
Full navigation with role-specific homes and menus:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
HOME PAGE MyModule.AdminHome FOR MyModule.Administrator
LOGIN PAGE Administration.Login
NOT FOUND PAGE MyModule.Custom404
MENU (
MENU ITEM 'Home' PAGE MyModule.Home_Web;
MENU 'Admin' (
MENU ITEM 'Users' PAGE Administration.Account_Overview;
MENU ITEM 'Settings' PAGE MyModule.Settings;
);
MENU ITEM 'About' PAGE MyModule.About;
);
Native mobile navigation:
CREATE OR REPLACE NAVIGATION NativePhone
HOME PAGE Mobile.Dashboard
MENU (
MENU ITEM 'Home' PAGE Mobile.Dashboard;
MENU ITEM 'Tasks' PAGE Mobile.TaskList;
MENU ITEM 'Profile' PAGE Mobile.UserProfile;
);
See Also
SHOW NAVIGATION
Synopsis
SHOW NAVIGATION
SHOW NAVIGATION MENU [ profile ]
SHOW NAVIGATION HOMES
DESCRIBE NAVIGATION [ profile ]
Description
Displays navigation configuration for the current project.
SHOW NAVIGATION
Displays a summary of all configured navigation profiles, showing the profile type and whether a home page, login page, and menu are configured.
SHOW NAVIGATION MENU
Displays the menu tree for one or all navigation profiles. When a profile is specified, only that profile’s menu is shown. Without a profile, all profiles’ menus are displayed.
The menu tree is rendered as an indented hierarchy showing menu labels and their target pages.
SHOW NAVIGATION HOMES
Displays home page assignments across all navigation profiles, including role-specific overrides.
DESCRIBE NAVIGATION
Outputs the full MDL representation of one or all navigation profiles. The output is round-trippable – it can be re-executed with CREATE OR REPLACE NAVIGATION to recreate the profile.
Parameters
profile- Optional. One of:
Responsive,Tablet,Phone,NativePhone. When omitted, all profiles are shown.
Examples
Show a summary of all profiles:
SHOW NAVIGATION;
Show the menu tree for the responsive profile:
SHOW NAVIGATION MENU Responsive;
Show all home page assignments:
SHOW NAVIGATION HOMES;
Export the responsive profile as MDL:
DESCRIBE NAVIGATION Responsive;
See Also
Workflow Statements
Statements for creating, inspecting, and dropping workflows.
Mendix workflows model long-running business processes with user tasks, decisions, parallel splits, and other activity types. A workflow is parameterized by a context entity that carries the data flowing through the process.
Statements
| Statement | Description |
|---|---|
| CREATE WORKFLOW | Define a workflow with activities, user tasks, decisions, and parallel paths |
| DROP WORKFLOW | Remove a workflow from the project |
Related Statements
| Statement | Syntax |
|---|---|
| Show workflows | SHOW WORKFLOWS [IN module] |
| Describe workflow | DESCRIBE WORKFLOW module.Name |
| Grant workflow access | GRANT EXECUTE ON WORKFLOW module.Name TO module.Role, ... |
| Revoke workflow access | REVOKE EXECUTE ON WORKFLOW module.Name FROM module.Role, ... |
CREATE WORKFLOW
Synopsis
CREATE [ OR MODIFY ] WORKFLOW module.Name
PARAMETER $Variable: module.Entity
[ OVERVIEW PAGE module.PageName ]
[ DISPLAY 'caption' ]
[ DESCRIPTION 'text' ]
BEGIN
activities
END WORKFLOW
Description
Creates a workflow that models a long-running business process. Each workflow has a context parameter (an entity that carries the data through the process) and a sequence of activities.
The OR MODIFY option updates an existing workflow if one exists with the same name, or creates a new one if it does not.
Activity Types
The following activity types can appear inside a workflow body:
- USER TASK
- An activity that requires human action. A user task displays a page to an assigned user and waits for them to select an outcome. Supports targeting by microflow or XPath to determine the assignee.
USER TASK name 'caption'
[ PAGE module.PageName ]
[ TARGETING MICROFLOW module.MicroflowName ]
[ TARGETING XPATH 'expression' ]
[ ENTITY module.Entity ]
[ DUE DATE 'expression' ]
[ DESCRIPTION 'text' ]
OUTCOMES 'OutcomeName' { activities } [ 'OutcomeName' { activities } ... ]
- MULTI USER TASK
- Same as USER TASK but assigned to multiple users. Uses the same syntax with
MULTI USER TASKinstead ofUSER TASK. - CALL MICROFLOW
- Calls a microflow as part of the workflow. The microflow can return a Boolean or enumeration to drive conditional outcomes.
CALL MICROFLOW module.Name [ 'caption' ]
[ WITH ( Parameter = expression [, ...] ) ]
[ OUTCOMES 'OutcomeName' { activities } ... ]
- CALL WORKFLOW
- Calls a sub-workflow.
CALL WORKFLOW module.Name [ 'caption' ]
[ WITH ( Parameter = expression [, ...] ) ]
- DECISION
- A conditional branch based on an expression. Each outcome maps to a different execution path.
DECISION [ 'caption' ]
OUTCOMES 'OutcomeName' { activities } [ 'OutcomeName' { activities } ... ]
- PARALLEL SPLIT
- Splits the workflow into parallel paths that execute concurrently and rejoin before continuing.
PARALLEL SPLIT [ 'caption' ]
PATH 1 { activities }
PATH 2 { activities }
[ PATH n { activities } ... ]
- JUMP TO
- Unconditionally jumps to a named activity earlier in the workflow, creating a loop.
JUMP TO activity_name [ 'caption' ]
- WAIT FOR TIMER
- Pauses the workflow until a timer expression elapses.
WAIT FOR TIMER [ 'duration_expression' ]
- WAIT FOR NOTIFICATION
- Pauses the workflow until an external notification resumes it.
WAIT FOR NOTIFICATION
- END
- Explicitly terminates a workflow path.
END
Boundary Events
User tasks, call microflow activities, and wait-for-notification activities support boundary events that trigger alternative flows when a timer expires:
USER TASK name 'caption'
PAGE module.Page
OUTCOMES 'Done' { }
BOUNDARY EVENT InterruptingTimer '${PT1H}' { activities }
BOUNDARY EVENT NonInterruptingTimer '${PT30M}' { activities }
Parameters
module.Name- Qualified name of the workflow (
Module.WorkflowName). PARAMETER $Variable: module.Entity- The context parameter. The variable name (prefixed with
$) and entity type define the data that flows through the workflow. The entity must already exist. OVERVIEW PAGE module.PageName- Optional overview page displayed when viewing workflow instances.
DISPLAY 'caption'- Optional display name shown in the workflow overview.
DESCRIPTION 'text'- Optional description of the workflow’s purpose.
Examples
Simple approval workflow:
CREATE WORKFLOW HR.ApprovalFlow
PARAMETER $Context: HR.LeaveRequest
OVERVIEW PAGE HR.WorkflowOverview
BEGIN
USER TASK ReviewTask 'Review the request'
PAGE HR.ReviewPage
OUTCOMES 'Approve' { } 'Reject' { };
END WORKFLOW;
Workflow with decision and parallel paths:
CREATE WORKFLOW Sales.OrderProcessing
PARAMETER $Context: Sales.Order
BEGIN
CALL MICROFLOW Sales.ACT_ValidateOrder 'Validate'
OUTCOMES 'True' {
PARALLEL SPLIT
PATH 1 {
CALL MICROFLOW Sales.ACT_ReserveInventory;
}
PATH 2 {
CALL MICROFLOW Sales.ACT_ProcessPayment;
};
USER TASK ShipTask 'Arrange shipping'
PAGE Sales.ShippingPage
TARGETING MICROFLOW Sales.ACT_GetWarehouseStaff
OUTCOMES 'Shipped' { };
}
'False' {
CALL MICROFLOW Sales.ACT_NotifyCustomer;
};
END WORKFLOW;
Workflow with a timer boundary event:
CREATE WORKFLOW Support.TicketEscalation
PARAMETER $Context: Support.Ticket
BEGIN
USER TASK AssignTask 'Handle ticket'
PAGE Support.TicketPage
OUTCOMES 'Resolved' { }
BOUNDARY EVENT InterruptingTimer '${PT24H}' {
CALL MICROFLOW Support.ACT_EscalateTicket;
};
END WORKFLOW;
See Also
DROP WORKFLOW
Synopsis
DROP WORKFLOW module.Name
Description
Removes a workflow from the project. The workflow is identified by its qualified name. If the workflow does not exist, an error is returned.
Any references to the dropped workflow (e.g., CALL WORKFLOW activities in other workflows, or navigation entries) will become broken and should be updated or removed.
Parameters
module.Name- The qualified name of the workflow to drop (
Module.WorkflowName).
Examples
DROP WORKFLOW HR.ApprovalFlow;
See Also
Business Event Statements
Statements for managing business event services.
Business event services enable asynchronous, event-driven communication between Mendix applications (and external systems) using the Mendix Business Events platform. A service defines named messages with typed attributes and specifies whether the application publishes or subscribes to each message.
Statements
| Statement | Description |
|---|---|
| CREATE BUSINESS EVENT SERVICE | Define a business event service with messages |
| DROP BUSINESS EVENT SERVICE | Remove a business event service |
Related Statements
| Statement | Syntax |
|---|---|
| Show business events | SHOW BUSINESS EVENTS [IN module] |
| Describe service | DESCRIBE BUSINESS EVENT SERVICE module.Name |
CREATE BUSINESS EVENT SERVICE
Synopsis
CREATE [ OR REPLACE ] BUSINESS EVENT SERVICE module.Name
(
ServiceName: 'service_name',
EventNamePrefix: 'prefix'
)
{
MESSAGE MessageName ( attr: Type [, ...] ) PUBLISH | SUBSCRIBE
[ ENTITY module.Entity ] ;
[ ... ]
}
Description
Creates a business event service that defines one or more messages for event-driven communication between applications.
Each message has a name, a set of typed attributes, and an operation mode (PUBLISH or SUBSCRIBE). A publishing message means this application sends events of that type. A subscribing message means this application receives and handles events of that type.
The OR REPLACE option drops any existing service with the same name before creating the new one.
Service Properties
The service declaration includes two required properties:
- ServiceName – The logical service name used for event routing on the Mendix Business Events broker.
- EventNamePrefix – A prefix prepended to message names to form the fully qualified event name. Can be empty (
'').
Message Attributes
Each message defines zero or more attributes with the following supported types:
| Type | Description |
|---|---|
String | Text value |
Integer | 32-bit integer |
Long | 64-bit integer |
Boolean | True/false |
DateTime | Date and time |
Decimal | Arbitrary-precision decimal |
Entity Mapping
The optional ENTITY clause on a message links the message to a Mendix entity. For SUBSCRIBE messages, incoming events are mapped to instances of this entity. For PUBLISH messages, entity instances are serialized into outgoing events.
Parameters
module.Name- Qualified name of the business event service (
Module.ServiceName). ServiceName: 'service_name'- The logical service identifier used by the Business Events broker.
EventNamePrefix: 'prefix'- A prefix for event names. Use an empty string (
'') for no prefix. MESSAGE MessageName- The name of a message within the service.
attr: Type- An attribute definition within a message. Multiple attributes are comma-separated.
PUBLISH- This application publishes (sends) this message type.
SUBSCRIBE- This application subscribes to (receives) this message type.
ENTITY module.Entity- Optional entity linked to the message for data mapping.
Examples
Create a service that publishes order events:
CREATE BUSINESS EVENT SERVICE Shop.OrderEvents
(
ServiceName: 'com.example.shop.orders',
EventNamePrefix: 'shop'
)
{
MESSAGE OrderCreated (OrderId: Long, CustomerName: String, Total: Decimal) PUBLISH
ENTITY Shop.Order;
MESSAGE OrderShipped (OrderId: Long, TrackingNumber: String) PUBLISH
ENTITY Shop.Shipment;
};
Create a service that subscribes to external events:
CREATE BUSINESS EVENT SERVICE Inventory.StockUpdates
(
ServiceName: 'com.example.warehouse.stock',
EventNamePrefix: ''
)
{
MESSAGE StockChanged (ProductId: Long, NewQuantity: Integer) SUBSCRIBE
ENTITY Inventory.StockLevel;
};
Replace an existing service:
CREATE OR REPLACE BUSINESS EVENT SERVICE Shop.OrderEvents
(
ServiceName: 'com.example.shop.orders',
EventNamePrefix: 'shop'
)
{
MESSAGE OrderCreated (OrderId: Long, CustomerName: String, Total: Decimal) PUBLISH;
MESSAGE OrderCancelled (OrderId: Long, Reason: String) PUBLISH;
};
See Also
DROP BUSINESS EVENT SERVICE
Synopsis
DROP BUSINESS EVENT SERVICE module.Name
Description
Removes a business event service from the project. The service is identified by its qualified name. If the service does not exist, an error is returned.
Dropping a service removes all its message definitions and operation implementations. Any microflows that reference the dropped service’s messages will need to be updated.
Parameters
module.Name- The qualified name of the business event service to drop (
Module.ServiceName).
Examples
DROP BUSINESS EVENT SERVICE Shop.OrderEvents;
See Also
Image Collection Statements
Statements for creating, inspecting, and dropping image collections within modules.
| Statement | Description |
|---|---|
| CREATE IMAGE COLLECTION | Create a new image collection with optional images |
| DROP IMAGE COLLECTION | Remove an image collection |
| SHOW / DESCRIBE IMAGE COLLECTION | List or inspect image collections |
CREATE IMAGE COLLECTION
Synopsis
CREATE IMAGE COLLECTION module.name
[EXPORT LEVEL 'Hidden' | 'Public']
[COMMENT 'description']
[(
IMAGE 'image_name' FROM FILE 'path',
...
)];
Description
Creates a new image collection in the specified module. Image collections bundle images (icons, logos, graphics) within a module. Images can be loaded from the filesystem during creation, with the format detected automatically from the file extension.
Parameters
- module.name
- The qualified name of the collection (e.g.,
MyModule.AppIcons). - EXPORT LEVEL
- Controls visibility from other modules.
'Hidden'(default) restricts access to the owning module.'Public'makes images available to other modules. - COMMENT
- Documentation text for the collection.
- IMAGE ‘name’ FROM FILE ‘path’
- Loads an image from a file on disk. The path is relative to the current working directory. Supported formats: PNG, SVG, GIF, JPEG, BMP, WebP.
Examples
Empty collection
CREATE IMAGE COLLECTION MyModule.AppIcons;
Public collection with description
CREATE IMAGE COLLECTION MyModule.SharedIcons
EXPORT LEVEL 'Public'
COMMENT 'Shared icons for all modules';
Collection with images
CREATE IMAGE COLLECTION MyModule.NavigationIcons (
IMAGE 'home' FROM FILE 'assets/home.png',
IMAGE 'settings' FROM FILE 'assets/settings.svg',
IMAGE 'profile' FROM FILE 'assets/profile.png'
);
All options combined
CREATE IMAGE COLLECTION MyModule.BrandAssets
EXPORT LEVEL 'Public'
COMMENT 'Company branding assets' (
IMAGE 'logo-dark' FROM FILE 'assets/logo-dark.png',
IMAGE 'logo-light' FROM FILE 'assets/logo-light.png'
);
See Also
DROP IMAGE COLLECTION, SHOW / DESCRIBE IMAGE COLLECTION
DROP IMAGE COLLECTION
Synopsis
DROP IMAGE COLLECTION module.name;
Description
Removes an image collection and all its embedded images from the module.
Parameters
- module.name
- The qualified name of the collection to drop (e.g.,
MyModule.AppIcons).
Examples
DROP IMAGE COLLECTION MyModule.StatusIcons;
Check references before dropping
SHOW REFERENCES TO MyModule.AppIcons;
DROP IMAGE COLLECTION MyModule.AppIcons;
See Also
CREATE IMAGE COLLECTION, SHOW / DESCRIBE IMAGE COLLECTION
SHOW / DESCRIBE IMAGE COLLECTION
Synopsis
SHOW IMAGE COLLECTION;
SHOW IMAGE COLLECTION IN module;
DESCRIBE IMAGE COLLECTION module.name;
Description
SHOW IMAGE COLLECTION lists all image collections in the project, optionally filtered by module. DESCRIBE IMAGE COLLECTION shows the full definition of a specific collection, including its images as a re-executable CREATE statement.
In the TUI, images are rendered inline when the terminal supports it (Kitty, iTerm2, Sixel).
Parameters
- IN module
- Filters the listing to a single module.
- module.name
- The qualified name of the collection to describe (e.g.,
MyModule.AppIcons).
Examples
List all image collections
SHOW IMAGE COLLECTION;
Filter by module
SHOW IMAGE COLLECTION IN MyModule;
View full definition
DESCRIBE IMAGE COLLECTION MyModule.AppIcons;
The output includes the complete CREATE statement with all IMAGE ... FROM FILE entries, which can be copied and re-executed.
See Also
CREATE IMAGE COLLECTION, DROP IMAGE COLLECTION
Catalog Statements
The catalog is a SQLite database that caches project metadata for fast querying and cross-reference navigation. It is stored in .mxcli/catalog.db next to the MPR file.
A basic REFRESH CATALOG populates tables for modules, entities, attributes, associations, microflows, nanoflows, pages, snippets, enumerations, and workflows. A REFRESH CATALOG FULL additionally populates cross-references (REFS), widgets, strings, source text, and permissions – enabling callers/callees analysis, impact analysis, and full-text search.
| Statement | Description |
|---|---|
| REFRESH CATALOG | Rebuild the catalog from the current project state |
| SELECT FROM CATALOG | Query catalog tables with SQL syntax |
| SHOW CATALOG TABLES | List available catalog tables and their columns |
| SHOW CALLERS / CALLEES | Find what calls an element or what it calls |
| SHOW REFERENCES / IMPACT / CONTEXT | Cross-reference navigation and impact analysis |
REFRESH CATALOG
Synopsis
REFRESH CATALOG [ FULL ] [ FORCE ]
Description
Rebuilds the project metadata catalog from the current state of the open project. The catalog is a SQLite database (.mxcli/catalog.db) that enables fast SQL querying and cross-reference navigation over project elements.
Without any options, REFRESH CATALOG performs a basic rebuild that populates the core metadata tables: MODULES, ENTITIES, ATTRIBUTES, ASSOCIATIONS, MICROFLOWS, NANOFLOWS, PAGES, SNIPPETS, ENUMERATIONS, and WORKFLOWS.
With the FULL option, the rebuild additionally populates cross-reference tables (REFS), widget inventories (WIDGETS), string tables (STRINGS), source text (SOURCE), activity tables (ACTIVITIES), and permission tables (PERMISSIONS). The FULL rebuild is required before using SHOW CALLERS, SHOW CALLEES, SHOW REFERENCES, SHOW IMPACT, SHOW CONTEXT, or SEARCH.
The catalog is cached between sessions. If the project has not changed, repeated REFRESH CATALOG calls reuse the cached data. Use FORCE to bypass the cache and rebuild unconditionally – useful after external changes to the MPR file.
Parameters
- FULL
- Include cross-references, widgets, strings, source text, and permissions in the catalog. Required for callers/callees analysis and full-text search.
- FORCE
- Bypass the catalog cache and force a complete rebuild regardless of whether the project appears unchanged.
Examples
Basic catalog refresh
REFRESH CATALOG;
Full rebuild with cross-references
REFRESH CATALOG FULL;
Force a complete rebuild
REFRESH CATALOG FULL FORCE;
Refresh from the command line
-- Shell commands:
-- mxcli -p app.mpr -c "REFRESH CATALOG"
-- mxcli -p app.mpr -c "REFRESH CATALOG FULL"
-- mxcli -p app.mpr -c "REFRESH CATALOG FULL FORCE"
See Also
SELECT FROM CATALOG, SHOW CATALOG TABLES, SHOW CALLERS / CALLEES
SELECT FROM CATALOG
Synopsis
SELECT columns FROM CATALOG.table [ WHERE condition ] [ ORDER BY column [ ASC | DESC ] ] [ LIMIT n ]
Description
Executes a SQL query against the project metadata catalog. The catalog tables are populated by REFRESH CATALOG and can be queried using standard SQL syntax including joins, aggregations, subqueries, and filtering.
All catalog table names are prefixed with CATALOG. (e.g., CATALOG.ENTITIES, CATALOG.MICROFLOWS). The query engine is SQLite, so standard SQLite SQL syntax applies.
The following tables are available after a basic REFRESH CATALOG:
| Table | Description |
|---|---|
CATALOG.MODULES | Project modules |
CATALOG.ENTITIES | Entities across all modules |
CATALOG.ATTRIBUTES | Entity attributes |
CATALOG.ASSOCIATIONS | Associations between entities |
CATALOG.MICROFLOWS | Microflows |
CATALOG.NANOFLOWS | Nanoflows |
CATALOG.PAGES | Pages |
CATALOG.SNIPPETS | Snippets |
CATALOG.ENUMERATIONS | Enumerations |
CATALOG.WORKFLOWS | Workflows |
The following tables require REFRESH CATALOG FULL:
| Table | Description |
|---|---|
CATALOG.ACTIVITIES | Individual microflow/nanoflow activities |
CATALOG.WIDGETS | Widget instances across pages and snippets |
CATALOG.REFS | Cross-references between elements |
CATALOG.PERMISSIONS | Access rules and role permissions |
CATALOG.STRINGS | All string values in the project |
CATALOG.SOURCE | Source text of microflows, pages, etc. |
Parameters
- columns
- Column names or expressions to select. Use
*for all columns, or specify individual column names. Supports SQL functions likeCOUNT(),SUM(),GROUP_CONCAT(). - table
- A catalog table name (e.g.,
ENTITIES,MICROFLOWS). Must be prefixed withCATALOG.. - condition
- A SQL WHERE clause for filtering rows. Supports standard operators (
=,LIKE,IN,AND,OR) and subqueries. - column
- Column to sort by. Append
ASC(default) orDESCfor sort direction. - n
- Maximum number of rows to return.
Examples
List all entities
SELECT Name, Module FROM CATALOG.ENTITIES;
Find microflows in a specific module
SELECT Name FROM CATALOG.MICROFLOWS WHERE Module = 'Sales' ORDER BY Name;
Count entities per module
SELECT Module, COUNT(*) AS EntityCount
FROM CATALOG.ENTITIES
GROUP BY Module
ORDER BY EntityCount DESC;
Find entities with many attributes
SELECT e.Module, e.Name, COUNT(a.Name) AS AttrCount
FROM CATALOG.ENTITIES e
JOIN CATALOG.ATTRIBUTES a ON e.Id = a.EntityId
GROUP BY e.Module, e.Name
HAVING COUNT(a.Name) > 10
ORDER BY AttrCount DESC;
Find many-to-many associations
SELECT Name, ParentEntity, ChildEntity
FROM CATALOG.ASSOCIATIONS
WHERE Type = 'ReferenceSet';
Find pages with no microflow references
SELECT p.Module, p.Name
FROM CATALOG.PAGES p
WHERE p.Id NOT IN (
SELECT DISTINCT TargetId FROM CATALOG.REFS WHERE RefKind = 'page'
);
Limit results
SELECT Name FROM CATALOG.MICROFLOWS LIMIT 10;
See Also
REFRESH CATALOG, SHOW CATALOG TABLES, SEARCH
SHOW CALLERS / CALLEES
Synopsis
SHOW CALLERS OF qualified_name [ TRANSITIVE ]
SHOW CALLEES OF qualified_name [ TRANSITIVE ]
Description
Displays the call relationships for a given element. SHOW CALLERS finds all elements that call or reference the specified element (incoming edges). SHOW CALLEES finds all elements that the specified element calls or references (outgoing edges).
These commands require REFRESH CATALOG FULL to have been run beforehand. Without it, the cross-reference data needed for caller/callee analysis is not available.
By default, only direct (one-hop) relationships are shown. With the TRANSITIVE option, the command follows the call chain recursively to show the full transitive closure – all indirect callers or callees at any depth.
Parameters
- qualified_name
- The fully qualified name of the element to analyze (e.g.,
Module.MicroflowName,Module.EntityName). Works with microflows, nanoflows, pages, snippets, entities, and other referenceable elements. - TRANSITIVE
- Follow the call chain recursively. For
SHOW CALLERS, this finds everything that directly or indirectly leads to the target. ForSHOW CALLEES, this finds everything the target directly or indirectly depends on.
Examples
Find direct callers of a microflow
REFRESH CATALOG FULL;
SHOW CALLERS OF Sales.ACT_CreateOrder;
Find all transitive callers
SHOW CALLERS OF Sales.ACT_CreateOrder TRANSITIVE;
Find what a microflow calls
SHOW CALLEES OF Sales.ACT_ProcessOrder;
Find all transitive callees
SHOW CALLEES OF Sales.ACT_ProcessOrder TRANSITIVE;
From the command line
-- Shell commands:
-- mxcli callers -p app.mpr Sales.ACT_CreateOrder
-- mxcli callers -p app.mpr Sales.ACT_CreateOrder --transitive
-- mxcli callees -p app.mpr Sales.ACT_ProcessOrder
See Also
SHOW REFERENCES / IMPACT / CONTEXT, REFRESH CATALOG, SELECT FROM CATALOG
SHOW REFERENCES / IMPACT / CONTEXT
Synopsis
SHOW REFERENCES TO qualified_name
SHOW IMPACT OF qualified_name
SHOW CONTEXT OF qualified_name [ DEPTH n ]
Description
These commands provide different views of cross-reference information for a given element. All three require REFRESH CATALOG FULL to have been run beforehand.
SHOW REFERENCES TO lists all elements that reference the specified element. This includes microflows that use an entity, pages that display it, associations that connect to it, and any other form of reference.
SHOW IMPACT OF performs an impact analysis showing what would be affected if the specified element were changed or removed. This is broader than SHOW REFERENCES as it considers transitive dependencies and indirect effects.
SHOW CONTEXT OF assembles the surrounding context of an element – its definition, its callers, callees, and related elements – suitable for providing to an LLM or for understanding an element in its broader project context. The optional DEPTH parameter controls how many levels of related elements to include.
Parameters
- qualified_name
- The fully qualified name of the element to analyze (e.g.,
Module.EntityName,Module.MicroflowName). - n (CONTEXT only)
- The number of levels of related elements to include. Defaults to 1 if not specified. Higher values include more surrounding context but produce more output.
Examples
Find all references to an entity
REFRESH CATALOG FULL;
SHOW REFERENCES TO Sales.Customer;
Analyze impact before making changes
SHOW IMPACT OF Sales.Customer;
Gather context for a microflow
SHOW CONTEXT OF Sales.ACT_CreateOrder;
Gather deeper context
SHOW CONTEXT OF Sales.ACT_CreateOrder DEPTH 3;
Check impact before moving an element
SHOW IMPACT OF Sales.CustomerEdit;
MOVE PAGE Sales.CustomerEdit TO NewModule;
From the command line
-- Shell commands:
-- mxcli refs -p app.mpr Sales.Customer
-- mxcli impact -p app.mpr Sales.Customer
-- mxcli context -p app.mpr Sales.ACT_CreateOrder --depth 3
See Also
SHOW CALLERS / CALLEES, REFRESH CATALOG, SELECT FROM CATALOG
SHOW CATALOG TABLES
Synopsis
SHOW CATALOG TABLES
Description
Lists all available catalog tables along with their column names and types. This is useful for discovering what metadata is queryable before writing SELECT FROM CATALOG queries.
The output includes both basic tables (populated by REFRESH CATALOG) and extended tables (populated by REFRESH CATALOG FULL). Tables that require a full refresh are marked accordingly.
Parameters
This statement takes no parameters.
Examples
List all catalog tables
SHOW CATALOG TABLES;
Typical workflow: discover then query
REFRESH CATALOG;
SHOW CATALOG TABLES;
SELECT * FROM CATALOG.ENTITIES LIMIT 5;
See Also
REFRESH CATALOG, SELECT FROM CATALOG
External SQL Statements
Statements for connecting to and querying external databases (PostgreSQL, Oracle, SQL Server). Credentials are isolated from session output and logs – the DSN is never displayed after the initial connection.
External SQL connections are independent of the Mendix project connection. You can have multiple external database connections open simultaneously, each identified by an alias.
| Statement | Description |
|---|---|
| SQL CONNECT | Open a connection to an external database |
| SQL DISCONNECT | Close an external database connection |
| SQL (query) | Execute SQL queries and schema discovery commands |
| SQL GENERATE CONNECTOR | Generate Database Connector MDL from an external schema |
| IMPORT FROM | Import data from an external database into a Mendix app database |
SQL CONNECT
Synopsis
SQL CONNECT driver 'dsn' AS alias
Description
Opens a connection to an external database. The connection is identified by the alias, which is used in all subsequent SQL commands to route queries to the correct database.
Multiple connections can be open simultaneously with different aliases. Use SQL CONNECTIONS to list active connections (shows alias and driver only – the DSN is never displayed for security).
Supported database drivers:
| Driver | Aliases | Example DSN |
|---|---|---|
postgres | pg, postgresql | postgres://user:pass@host:5432/dbname |
oracle | ora | oracle://user:pass@host:1521/service |
sqlserver | mssql | sqlserver://user:pass@host:1433?database=dbname |
Parameters
- driver
- The database driver to use. One of
postgres(orpg,postgresql),oracle(orora), orsqlserver(ormssql). - dsn
- The data source name (connection string) enclosed in single quotes. The format depends on the driver. The DSN is stored in memory only and never appears in session output or logs.
- alias
- A short identifier for this connection. Used in all subsequent
SQL alias ...commands. Must be unique among active connections.
Examples
Connect to PostgreSQL
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
Connect to Oracle
SQL CONNECT oracle 'oracle://scott:tiger@dbhost:1521/ORCL' AS erp;
Connect to SQL Server
SQL CONNECT sqlserver 'sqlserver://sa:Password1@localhost:1433?database=northwind' AS legacy;
List active connections
SQL CONNECTIONS;
Connect using driver aliases
SQL CONNECT pg 'postgres://user:pass@localhost:5432/mydb' AS src;
SQL CONNECT mssql 'sqlserver://sa:Pass@host:1433?database=db' AS dst;
See Also
SQL DISCONNECT, SQL (query), SQL GENERATE CONNECTOR
SQL DISCONNECT
Synopsis
SQL DISCONNECT alias
Description
Closes an external database connection identified by the given alias. After disconnecting, the alias is no longer valid and any subsequent SQL alias ... commands will fail.
Use SQL CONNECTIONS to see which connections are currently active before disconnecting.
Parameters
- alias
- The alias of the connection to close. Must match an active connection established with
SQL CONNECT.
Examples
Disconnect a connection
SQL DISCONNECT source;
Full connection lifecycle
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
SQL source SHOW TABLES;
SQL source SELECT COUNT(*) FROM users;
SQL DISCONNECT source;
See Also
SQL (query)
Synopsis
SQL alias query_text
SQL alias SHOW TABLES
SQL alias SHOW VIEWS
SQL alias SHOW FUNCTIONS
SQL alias DESCRIBE table_name
Description
Executes a SQL query or schema discovery command against an external database connection. The alias identifies which connection to use (established with SQL CONNECT).
Raw SQL passthrough: Any SQL text after the alias is sent directly to the external database. The results are displayed as a formatted table. This supports SELECT, INSERT, UPDATE, DELETE, DDL, and any other SQL the target database accepts.
Schema discovery commands provide a portable way to explore the database schema without writing database-specific SQL:
| Command | Description |
|---|---|
SHOW TABLES | Lists user tables in the database |
SHOW VIEWS | Lists user views in the database |
SHOW FUNCTIONS | Lists functions and stored procedures |
DESCRIBE table_name | Shows columns, data types, and nullability for a table |
The schema discovery commands query information_schema internally and work consistently across PostgreSQL, Oracle, and SQL Server.
Parameters
- alias
- The connection alias established with
SQL CONNECT. - query_text
- Any valid SQL for the target database. Sent as-is to the database engine.
- table_name (DESCRIBE only)
- The name of the table or view to describe.
Examples
Query data
SQL source SELECT * FROM users WHERE active = true LIMIT 10;
List tables
SQL source SHOW TABLES;
List views
SQL source SHOW VIEWS;
Describe a table
SQL source DESCRIBE users;
Insert data
SQL source INSERT INTO audit_log (action, timestamp) VALUES ('export', NOW());
Run aggregate queries
SQL source SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department
ORDER BY headcount DESC;
From the command line
-- Shell command:
-- mxcli sql --driver postgres --dsn 'postgres://...' "SELECT * FROM users"
See Also
SQL CONNECT, SQL DISCONNECT, SQL GENERATE CONNECTOR, IMPORT FROM
SQL GENERATE CONNECTOR
Synopsis
SQL alias GENERATE CONNECTOR INTO module [ TABLES ( table [, ...] ) ] [ VIEWS ( view [, ...] ) ] [ EXEC ]
Description
Generates MDL statements that create a Mendix Database Connector module from the schema of an external database. The generated MDL includes entities, attributes, and associations that mirror the external database tables and views, configured for use with the Mendix Database Connector module.
Without EXEC, the command outputs the generated MDL to the console for review. With EXEC, it executes the MDL immediately against the open project.
By default, all user tables are included. Use TABLES (...) and VIEWS (...) to limit generation to specific tables and views.
The type mapping from SQL types to Mendix types is handled automatically (e.g., VARCHAR becomes String, INTEGER becomes Integer, TIMESTAMP becomes DateTime).
Parameters
- alias
- The connection alias established with
SQL CONNECT. - module
- The target Mendix module where the connector entities will be created.
- table
- One or more table names to include. If omitted, all user tables are included.
- view
- One or more view names to include. If omitted, no views are included unless
TABLESis also omitted (in which case all tables are included by default). - EXEC
- Execute the generated MDL immediately instead of printing it. Requires an open Mendix project.
Examples
Preview generated MDL for all tables
SQL source GENERATE CONNECTOR INTO HRModule;
Generate for specific tables
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments);
Generate for tables and views
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees) VIEWS (employee_summary);
Generate and execute immediately
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;
Full workflow
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/hrdb' AS source;
SQL source SHOW TABLES;
SQL source DESCRIBE employees;
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;
SQL DISCONNECT source;
See Also
SQL CONNECT, SQL (query), IMPORT FROM
IMPORT FROM
Synopsis
IMPORT FROM alias QUERY 'sql'
INTO module.Entity
MAP ( source_col AS AttrName [, ...] )
[ LINK ( source_col TO AssocName ON MatchAttr ) [, ...] ]
[ BATCH size ]
[ LIMIT count ]
Description
Imports data from an external database into a Mendix application’s PostgreSQL database. The command executes the specified SQL query against the external connection, maps the result columns to entity attributes, and inserts rows in batches using direct database insertion.
The import pipeline handles:
- Mendix ID generation: Automatically generates unique Mendix object IDs for each inserted row, following the Mendix ID format and sequence tracking.
- Batch insertion: Rows are inserted in configurable batch sizes for performance.
- Association linking: The
LINKclause matches imported rows to existing objects via an association, looking up the target by a matching attribute value.
The target Mendix application database is auto-detected from the project’s database configuration. The import connects directly to the application’s PostgreSQL database.
Parameters
- alias
- The external database connection alias established with
SQL CONNECT. - sql
- A SQL query string (in single quotes) to execute against the external database. The result set columns are available for mapping.
- module.Entity
- The fully qualified Mendix entity to import into (e.g.,
HR.Employee). - source_col
- A column name from the SQL query result set.
- AttrName
- The target attribute name on the Mendix entity.
- AssocName (LINK only)
- The name of the association to set. The source column value is matched against the
MatchAttron the associated entity to find the target object. - MatchAttr (LINK only)
- The attribute on the associated entity used to look up the target object for linking.
- size (BATCH only)
- Number of rows per insert batch. Defaults to a reasonable batch size if not specified.
- count (LIMIT only)
- Maximum number of rows to import from the query result.
Examples
Basic import
IMPORT FROM source QUERY 'SELECT name, email FROM employees'
INTO HR.Employee
MAP (name AS Name, email AS Email);
Import with association linking
IMPORT FROM source QUERY 'SELECT name, email, dept_name FROM employees'
INTO HR.Employee
MAP (name AS Name, email AS Email)
LINK (dept_name TO Employee_Department ON Name);
Import with batch size and limit
IMPORT FROM source QUERY 'SELECT name, email, dept_name FROM employees'
INTO HR.Employee
MAP (name AS Name, email AS Email)
LINK (dept_name TO Employee_Department ON Name)
BATCH 500
LIMIT 1000;
Full workflow: connect, explore, import
SQL CONNECT postgres 'postgres://user:pass@legacydb:5432/hr' AS source;
SQL source SHOW TABLES;
SQL source DESCRIBE employees;
IMPORT FROM source QUERY 'SELECT name, email FROM employees WHERE active = true'
INTO HR.Employee
MAP (name AS Name, email AS Email)
BATCH 500;
SQL DISCONNECT source;
See Also
SQL CONNECT, SQL (query), SQL GENERATE CONNECTOR
Settings Statements
Statements for viewing and modifying Mendix project settings. Settings are organized into categories: MODEL (application-level settings), CONFIGURATION (runtime configurations), CONSTANT (constant value overrides per configuration), LANGUAGE (localization settings), and WORKFLOWS (workflow engine settings).
| Statement | Description |
|---|---|
| SHOW / DESCRIBE SETTINGS | View current project settings |
| ALTER SETTINGS | Modify project settings by category |
SHOW / DESCRIBE SETTINGS
Synopsis
SHOW SETTINGS
DESCRIBE SETTINGS [ category ]
Description
Displays project settings. SHOW SETTINGS provides a compact overview of all settings across all categories. DESCRIBE SETTINGS outputs the full settings in round-trippable MDL syntax that can be used to recreate the same configuration.
When DESCRIBE SETTINGS is called without a category, it outputs all settings. When a category is specified, only that category is shown.
The available settings categories are:
| Category | Contents |
|---|---|
MODEL | Application-level settings: AfterStartupMicroflow, BeforeShutdownMicroflow, HashAlgorithm, JavaVersion, etc. |
CONFIGURATION | Runtime configurations: DatabaseType, DatabaseUrl, HttpPortNumber, etc. Each named configuration is listed separately. |
CONSTANT | Constant value overrides per configuration. Shows which constants have non-default values in each configuration. |
LANGUAGE | Localization settings: DefaultLanguageCode and available languages. |
WORKFLOWS | Workflow engine settings: UserEntity, DefaultTaskParallelism, etc. |
Parameters
- category (DESCRIBE only)
- One of
MODEL,CONFIGURATION,CONSTANT,LANGUAGE, orWORKFLOWS. If omitted, all categories are shown.
Examples
Show settings overview
SHOW SETTINGS;
Describe all settings in MDL format
DESCRIBE SETTINGS;
Describe model settings only
DESCRIBE SETTINGS MODEL;
Describe runtime configurations
DESCRIBE SETTINGS CONFIGURATION;
Describe workflow settings
DESCRIBE SETTINGS WORKFLOWS;
See Also
ALTER SETTINGS
Synopsis
ALTER SETTINGS MODEL key = value
ALTER SETTINGS CONFIGURATION 'name' key = value
ALTER SETTINGS CONSTANT 'name' VALUE 'value' IN CONFIGURATION 'config'
ALTER SETTINGS LANGUAGE key = value
ALTER SETTINGS WORKFLOWS key = value
Description
Modifies project settings by category. Each category has its own syntax and available keys.
MODEL settings control application-level behavior such as the after-startup microflow, hashing algorithm, and Java version.
CONFIGURATION settings control named runtime configurations. Each project can have multiple configurations (e.g., default, staging, production). Settings include database type, database URL, HTTP port number, and other runtime parameters. The configuration name must be enclosed in single quotes.
CONSTANT settings override the default value of a project constant within a specific runtime configuration. Both the constant name and the configuration name must be enclosed in single quotes.
LANGUAGE settings control localization, primarily the default language code.
WORKFLOWS settings control the workflow engine, including the user entity used for workflow tasks and default task parallelism.
Parameters
- key
- The setting name to modify. Available keys depend on the category.
- value
- The new value for the setting. String values must be enclosed in single quotes.
- name (CONFIGURATION, CONSTANT)
- The name of the configuration or constant, enclosed in single quotes.
- config (CONSTANT only)
- The name of the runtime configuration where the constant override applies, enclosed in single quotes.
Examples
Set the after-startup microflow
ALTER SETTINGS MODEL AfterStartupMicroflow = 'MyModule.ACT_Startup';
Configure database type
ALTER SETTINGS CONFIGURATION 'default' DatabaseType = 'POSTGRESQL';
Set database URL for a configuration
ALTER SETTINGS CONFIGURATION 'production' DatabaseUrl = 'jdbc:postgresql://dbhost:5432/myapp';
Override a constant in a configuration
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://api.staging.example.com' IN CONFIGURATION 'staging';
Set the default language
ALTER SETTINGS LANGUAGE DefaultLanguageCode = 'en_US';
Configure workflow user entity
ALTER SETTINGS WORKFLOWS UserEntity = 'Administration.Account';
Set Java version
ALTER SETTINGS MODEL JavaVersion = '17';
See Also
Organization Statements
Statements for organizing project structure: creating modules and folders, and moving documents between them.
| Statement | Description |
|---|---|
| CREATE MODULE | Create a new module in the project |
| CREATE FOLDER | Create a folder within a module for organizing documents |
| MOVE | Move a document to a different module or folder |
CREATE MODULE
Synopsis
CREATE MODULE module_name
Description
Creates a new module in the Mendix project. Modules are the top-level organizational unit for all project documents including entities, microflows, pages, and enumerations. The module is created with default settings and an empty domain model.
Parameters
- module_name
- The name of the module to create. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). If the name conflicts with a reserved keyword, enclose it in double quotes.
Examples
Create a module
CREATE MODULE OrderManagement;
Create multiple modules for a layered architecture
CREATE MODULE Sales;
CREATE MODULE Inventory;
CREATE MODULE Reporting;
See Also
CREATE FOLDER, MOVE, DROP MODULE
CREATE FOLDER
Synopsis
CREATE FOLDER module_name/folder_path
Description
Creates a folder within a module for organizing documents such as pages, microflows, and snippets. Nested folders use the / separator. If intermediate folders in the path do not exist, they are created automatically.
Note: Folders can also be created implicitly by specifying a FOLDER clause when creating a microflow or a Folder property when creating a page.
Parameters
- module_name
- The name of the module in which to create the folder.
- folder_path
- The path of the folder to create within the module. Use
/to create nested folders (e.g.,Orders/Processing).
Examples
Create a simple folder
CREATE FOLDER MyModule/Pages;
Create a nested folder
CREATE FOLDER MyModule/Orders/Processing;
Use a folder when creating a microflow
CREATE MICROFLOW MyModule.ACT_ProcessOrder
FOLDER 'Orders/Processing'
BEGIN
RETURN true;
END;
Use a folder when creating a page
CREATE PAGE MyModule.Order_Edit
(
Title: 'Edit Order',
Layout: Atlas_Core.PopupLayout,
Folder: 'Orders'
)
{
CONTAINER main () {}
}
See Also
MOVE
Synopsis
MOVE document_type qualified_name TO module_name
MOVE document_type qualified_name TO FOLDER 'folder_path'
MOVE document_type qualified_name TO FOLDER 'folder_path' IN module_name
Description
Moves a document to a different folder or module. Supported document types are PAGE, MICROFLOW, NANOFLOW, SNIPPET, ENUMERATION, and ENTITY. When moving to a folder, missing intermediate folders are created automatically. Cross-module moves change the qualified name of the document, which may break by-name references elsewhere in the project.
Entity moves only support moving to a module (not to a folder), because entities are embedded in domain model documents.
Parameters
- document_type
- The type of document to move. One of:
PAGE,MICROFLOW,NANOFLOW,SNIPPET,ENUMERATION,ENTITY. - qualified_name
- The current
Module.Nameof the document to move. - module_name
- The target module. When specified without
FOLDER, moves the document to the module root. - folder_path
- The target folder path within the module. Use
/for nested paths (e.g.,'Orders/Processing'). Enclosed in single quotes.
Examples
Move a page to a folder in the same module
MOVE PAGE MyModule.CustomerEdit TO FOLDER 'Customers';
Move a microflow to a nested folder
MOVE MICROFLOW MyModule.ACT_ProcessOrder TO FOLDER 'Orders/Processing';
Move a snippet to a different module
MOVE SNIPPET OldModule.NavigationMenu TO Common;
Move an entity to a different module
MOVE ENTITY OldModule.Customer TO NewModule;
Move a page to a folder in a different module
MOVE PAGE OldModule.CustomerPage TO FOLDER 'Screens' IN NewModule;
Check impact before a cross-module move
SHOW IMPACT OF OldModule.Customer;
MOVE ENTITY OldModule.Customer TO NewModule;
See Also
Session Statements
Statements for inspecting and configuring the current REPL session.
| Statement | Description |
|---|---|
| SET | Set a session variable |
| SHOW STATUS | Display the current connection and session information |
SET
Synopsis
SET variable = value
Description
Sets a session variable that controls REPL behavior. Session variables persist for the duration of the session and are reset when the REPL exits. Variable names are case-insensitive.
Parameters
- variable
- The name of the session variable to set.
- value
- The value to assign. Can be a string (in single quotes), a boolean (
TRUEorFALSE), or an unquoted identifier.
Recognized Variables
| Variable | Values | Description |
|---|---|---|
output_format / FORMAT | 'json', 'table' | Controls output format for query results |
verbose | TRUE, FALSE | Enable verbose output for debugging |
AUTOCOMMIT | TRUE, FALSE | Automatically save changes after each mutation |
Examples
Set output format to JSON
SET output_format = 'json';
Enable verbose mode
SET verbose = TRUE;
Enable autocommit
SET AUTOCOMMIT = TRUE;
See Also
SHOW STATUS
Synopsis
STATUS
Description
Displays the current session status including the connection state, the open project path, the Mendix version of the project, the MPR format version, and the values of any session variables. This is useful for confirming which project is loaded and verifying session configuration.
Parameters
This statement takes no parameters.
Examples
Check session status
STATUS;
Sample output:
Status: Connected
Project: /Users/dev/projects/MyApp/MyApp.mpr
Version: 10.6.0
Format: v2
Modules: 5
Verify connection before running commands
STATUS;
SHOW MODULES;
See Also
OPEN PROJECT, CLOSE PROJECT, SET
System Architecture
ModelSDK Go is a Go-native library for reading and modifying Mendix application projects (.mpr files). It provides programmatic access to Mendix projects without cloud connectivity, serving as an alternative to the TypeScript-based Mendix Model SDK.
High-Level Architecture
The system is organized into layered components, from user-facing interfaces down to file system storage:
graph TB
subgraph "User Interface"
CLI[mxcli CLI]
API[Go API]
VSCODE[VS Code Extension]
end
subgraph "MDL Layer"
REPL[REPL Interface]
LSP[LSP Server]
EXEC[Executor]
PARSER[ANTLR4 Parser]
AST[AST Nodes]
VISITOR[AST Visitor]
CATALOG[Catalog]
LINTER[Linter]
end
subgraph "High-Level API"
HLAPI[api/]
DMAPI[DomainModelsAPI]
MFAPI[MicroflowsAPI]
PGAPI[PagesAPI]
ENUMAPI[EnumerationsAPI]
MODAPI[ModulesAPI]
end
subgraph "SDK Layer"
SDK[modelsdk.go]
DM[domainmodel]
MF[microflows]
PG[pages]
MODEL[model]
WIDGETS[widgets]
end
subgraph "External SQL"
SQLPKG[sql/]
SQLCONN[Connection Manager]
SQLQUERY[Query Engine]
SQLIMPORT[Import Pipeline]
SQLGEN[Connector Generator]
end
subgraph "Storage Layer"
READER[MPR Reader]
WRITER[MPR Writer]
BSONP[BSON Parser]
end
subgraph "File System"
MPR[(MPR File)]
CONTENTS[(mprcontents/)]
EXTDB[(External DBs)]
end
CLI --> REPL
CLI --> LSP
API --> HLAPI
API --> SDK
VSCODE --> LSP
HLAPI --> SDK
HLAPI --> READER
HLAPI --> WRITER
REPL --> EXEC
LSP --> EXEC
EXEC --> PARSER
PARSER --> AST
AST --> VISITOR
VISITOR --> AST
EXEC --> SDK
EXEC --> SQLPKG
EXEC --> CATALOG
EXEC --> LINTER
SDK --> DM
SDK --> MF
SDK --> PG
SDK --> MODEL
SDK --> WIDGETS
SDK --> READER
SDK --> WRITER
SQLPKG --> SQLCONN
SQLPKG --> SQLQUERY
SQLPKG --> SQLIMPORT
SQLPKG --> SQLGEN
SQLCONN --> EXTDB
READER --> BSONP
WRITER --> BSONP
READER --> MPR
READER --> CONTENTS
WRITER --> MPR
WRITER --> CONTENTS
Layer Summary
| Layer | Purpose |
|---|---|
| User Interface | CLI (mxcli), Go API, and VS Code extension provide entry points for users and tools |
| MDL Layer | SQL-like language parser, executor, REPL, LSP server, catalog, and linter |
| High-Level API | Fluent builder API for domain models, microflows, pages, enumerations, and modules |
| SDK Layer | Core Go types and operations for Mendix model elements |
| External SQL | Database connectivity for PostgreSQL, Oracle, and SQL Server |
| Storage Layer | MPR file reading, writing, and BSON parsing |
| File System | Physical .mpr files, mprcontents/ directories, and external databases |
Key Design Decisions
-
ANTLR4 for Grammar – Enables cross-language grammar sharing (Go, TypeScript, Java) with case-insensitive keywords and built-in error recovery.
-
BSON for Serialization – Native Mendix format compatibility using
go.mongodb.org/mongo-driver/bson, handling polymorphic types via$Typefields. -
Two-Phase Loading – Units are loaded on-demand for performance, with lazy loading of related documents.
-
Interface-Based Design –
ElementandAttributeTypeinterfaces enable type-safe polymorphic operations across element types. -
Pure Go / No CGO – Uses
modernc.org/sqlite(pure Go SQLite) to eliminate the C compiler dependency, simplifying cross-compilation and deployment. -
Widget Template System – Pluggable widgets require complex BSON with internal ID references. Embedded JSON templates extracted from Studio Pro are cloned and customized at runtime.
-
Credential Isolation – External database credentials resolve from environment variables or YAML config, never stored in MDL scripts.
Layer Diagram
Detailed description of each architectural layer in ModelSDK Go.
1. Command Layer (cmd/)
| Package | Purpose |
|---|---|
cmd/mxcli | CLI entry point using Cobra framework; includes LSP server, Docker integration, diagnostics |
cmd/codegen | Metamodel code generator from reflection data |
Key CLI subcommands:
| Subcommand | File | Purpose |
|---|---|---|
exec | cmd_exec.go | Execute MDL script files |
check | cmd_check.go | Syntax and reference validation |
lint | cmd_lint.go | Run linting rules |
report | cmd_report.go | Best practices report |
test | cmd_test_run.go | Run .test.mdl / .test.md tests |
diff | cmd_diff.go | Compare script against project |
sql | cmd_sql.go | External SQL queries |
lsp | lsp.go | Language Server Protocol server |
init | init.go | Project initialization |
docker | docker.go | Docker build/check/OQL integration |
diag | diag.go | Session logs, bug report bundles |
2. MDL Layer (mdl/)
The MDL (Mendix Definition Language) layer provides a SQL-like interface for querying and modifying Mendix models.
sequenceDiagram
participant User
participant REPL
participant Parser
participant Visitor
participant Executor
participant SDK
User->>REPL: "SHOW ENTITIES IN MyModule"
REPL->>Parser: Parse MDL command
Parser->>Visitor: Walk parse tree
Visitor->>Executor: Build AST node
Executor->>SDK: ListDomainModels()
SDK-->>Executor: []*DomainModel
Executor-->>REPL: Formatted output
REPL-->>User: Display table
| Package | Purpose |
|---|---|
mdl/grammar | ANTLR4 lexer/parser (generated from MDLLexer.g4 + MDLParser.g4) |
mdl/ast | AST node types for MDL statements |
mdl/visitor | ANTLR listener that builds AST from parse tree |
mdl/executor | Executes AST nodes against the SDK (~45k lines across 40+ files) |
mdl/catalog | SQLite-based catalog for querying project metadata |
mdl/linter | Extensible linting framework with built-in rules and Starlark scripting |
mdl/repl | Interactive REPL interface |
3. High-Level API Layer (api/)
The api/ package provides a simplified, fluent builder API inspired by the Mendix Web Extensibility Model API.
classDiagram
class ModelAPI {
+writer *Writer
+reader *Reader
+currentModule *Module
+DomainModels *DomainModelsAPI
+Microflows *MicroflowsAPI
+Pages *PagesAPI
+Enumerations *EnumerationsAPI
+Modules *ModulesAPI
+New(writer) *ModelAPI
+SetModule(module) *ModelAPI
}
class EntityBuilder {
+Persistent() *EntityBuilder
+NonPersistent() *EntityBuilder
+WithStringAttribute(name, length) *EntityBuilder
+WithIntegerAttribute(name) *EntityBuilder
+Build() (*Entity, error)
}
class MicroflowBuilder {
+WithParameter(name, entity) *MicroflowBuilder
+WithStringParameter(name) *MicroflowBuilder
+ReturnsBoolean() *MicroflowBuilder
+Build() (*Microflow, error)
}
ModelAPI --> EntityBuilder : creates
ModelAPI --> MicroflowBuilder : creates
| File | Purpose |
|---|---|
api/api.go | ModelAPI entry point with namespace access |
api/domainmodels.go | EntityBuilder, AssociationBuilder, AttributeBuilder |
api/enumerations.go | EnumerationBuilder, EnumValueBuilder |
api/microflows.go | MicroflowBuilder with parameters and return types |
api/pages.go | PageBuilder, widget builders |
api/modules.go | ModulesAPI for module retrieval |
4. SDK Layer (sdk/)
The SDK layer provides Go types and APIs for Mendix model elements.
classDiagram
class Reader {
+Open(path) Reader
+ListModules() []*Module
+ListDomainModels() []*DomainModel
+ListMicroflows() []*Microflow
+ListPages() []*Page
+FindCustomWidgetType(widgetID) *RawCustomWidgetType
+Close()
}
class Writer {
+OpenForWriting(path) Writer
+AddEntity(dm, entity)
+AddAssociation(dm, assoc)
+Save()
+Close()
}
class DomainModel {
+ID
+ContainerID
+Entities []*Entity
+Associations []*Association
}
class Entity {
+ID
+Name
+Attributes []*Attribute
+Source string
+OqlQuery string
}
Reader --> DomainModel
Writer --> DomainModel
DomainModel --> Entity
| Package | Purpose |
|---|---|
sdk/mpr/ | MPR file format handling (~18k lines across reader, writer, parser files split by domain) |
sdk/domainmodel | Entity, Attribute, Association types |
sdk/microflows | Microflow, Activity types (60+ types) |
sdk/pages | Page, Widget types (50+ types) |
sdk/widgets | Embedded widget templates for pluggable widgets |
The sdk/mpr/ package is split by domain for maintainability:
| File Pattern | Purpose |
|---|---|
reader.go, reader_*.go | Read-only MPR access, split by element type |
writer.go, writer_*.go | Read-write MPR modification (domainmodel, microflow, security, widgets, etc.) |
parser.go, parser_*.go | BSON parsing and deserialization (domainmodel, microflow, etc.) |
utils.go | UUID generation utilities |
5. Model Layer (model/)
Core types shared across the SDK:
classDiagram
class ID {
<<type alias>>
string
}
class BaseElement {
+ID ID
+TypeName string
}
class QualifiedName {
+Module string
+Name string
}
class Module {
+BaseElement
+Name string
+FromAppStore bool
}
class Text {
+Translations map
+GetTranslation(lang) string
}
BaseElement <|-- Module
6. External SQL Layer (sql/)
The sql/ package provides external database connectivity for querying PostgreSQL, Oracle, and SQL Server databases.
flowchart LR
subgraph "MDL Commands"
CONNECT["SQL CONNECT"]
QUERY["SQL alias SELECT ..."]
IMPORT["IMPORT FROM alias ..."]
GENERATE["SQL alias GENERATE CONNECTOR"]
end
subgraph "sql/ Package"
CONN[Connection Manager]
QE[Query Engine]
IMP[Import Pipeline]
GEN[Connector Generator]
META[Schema Introspection]
FMT[Output Formatters]
end
subgraph "Databases"
PG[(PostgreSQL)]
ORA[(Oracle)]
MSSQL[(SQL Server)]
end
CONNECT --> CONN
QUERY --> QE
IMPORT --> IMP
GENERATE --> GEN
CONN --> PG
CONN --> ORA
CONN --> MSSQL
QE --> CONN
IMP --> CONN
GEN --> META
META --> CONN
| File | Purpose |
|---|---|
driver.go | DriverName type, ParseDriver() |
connection.go | Manager, Connection, credential isolation |
config.go | DSN resolution (env vars, YAML config) |
query.go | Execute() – query via database/sql |
meta.go | ShowTables(), DescribeTable() via information_schema |
import.go | IMPORT pipeline: batch insert, ID generation, sequence tracking |
generate.go | Database Connector MDL generation from external schema |
typemap.go | SQL to Mendix type mapping, DSN to JDBC URL conversion |
mendix.go | Mendix DB DSN builder, table/column name helpers |
format.go | Table and JSON output formatters |
7. VS Code Extension (vscode-mdl/)
The VS Code extension provides MDL language support via an LSP client that communicates with mxcli lsp --stdio.
graph TB
subgraph "VS Code"
EXT[extension.ts]
TREE[Project Tree Provider]
LINK[Terminal Link Provider]
CONTENT[MDL Content Provider]
PREVIEW[Preview Provider]
end
subgraph "Preview Renderers"
DMRENDER[Domain Model]
MFRENDER[Microflow]
PGRENDER[Page Wireframe]
MODRENDER[Module Overview]
QPRENDER[Query Plan]
end
subgraph "mxcli"
LSP[LSP Server]
end
EXT --> TREE
EXT --> LINK
EXT --> CONTENT
EXT --> PREVIEW
PREVIEW --> DMRENDER
PREVIEW --> MFRENDER
PREVIEW --> PGRENDER
PREVIEW --> MODRENDER
PREVIEW --> QPRENDER
EXT -->|stdio| LSP
LSP features include syntax highlighting, parse/semantic diagnostics, completion, symbols, folding, hover, go-to-definition, clickable terminal links, and context menu commands.
8. LSP Server (cmd/mxcli/lsp*.go)
The LSP server is embedded in the mxcli binary:
| File | Purpose |
|---|---|
lsp.go | Main LSP server, hover, go-to-definition |
lsp_diagnostics.go | Parse and semantic error reporting |
lsp_completion.go | Context-aware completions |
lsp_completions_gen.go | Generated completion data |
lsp_symbols.go | Document symbols |
lsp_folding.go | Code folding ranges |
lsp_hover.go | Hover information |
lsp_helpers.go | Shared utilities |
Package Structure
Overview of the Go package organization and dependency relationships.
Package Dependency Graph
graph LR
subgraph "cmd/"
MXCLI[mxcli/]
CODEGEN[codegen/]
end
subgraph "mdl/"
GRAMMAR[grammar/]
ASTPKG[ast/]
VISITORPKG[visitor/]
EXECUTORPKG[executor/]
CATALOGPKG[catalog/]
LINTERPKG[linter/]
REPLPKG[repl/]
end
subgraph "api/"
APIPKG[api.go]
DMAPIMOD[domainmodels.go]
MFAPIMOD[microflows.go]
PGAPIMOD[pages.go]
ENUMAPIMOD[enumerations.go]
MODAPIMOD[modules.go]
end
subgraph "sdk/"
MPR[mpr/]
DMPKG[domainmodel/]
MFPKG[microflows/]
PGPKG[pages/]
WIDGETSPKG[widgets/]
end
subgraph "sql/"
SQLCONN[connection.go]
SQLQUERY[query.go]
SQLIMPORT[import.go]
SQLGEN[generate.go]
SQLMETA[meta.go]
end
subgraph "internal/"
CODEGENINT[codegen/]
end
subgraph "vscode-mdl/"
VSEXT[extension.ts]
VSLSP[LSP client]
VSPREVIEW[preview/]
VSTREE[projectTreeProvider.ts]
end
subgraph "Root"
MODELSDK[modelsdk.go]
MODELPKG[model/]
end
MXCLI --> REPLPKG
MXCLI --> EXECUTORPKG
REPLPKG --> EXECUTORPKG
EXECUTORPKG --> VISITORPKG
VISITORPKG --> GRAMMAR
EXECUTORPKG --> MODELSDK
EXECUTORPKG --> CATALOGPKG
EXECUTORPKG --> LINTERPKG
APIPKG --> MPR
APIPKG --> DMPKG
APIPKG --> MFPKG
APIPKG --> PGPKG
MODELSDK --> MPR
MODELSDK --> DMPKG
MODELSDK --> MFPKG
MODELSDK --> PGPKG
MPR --> MODELPKG
VSEXT --> VSLSP
Directory Layout
github.com/mendixlabs/mxcli/
├── modelsdk.go # Main public API (Open, OpenForWriting, helpers)
├── model/ # Core types: ID, QualifiedName, Module, Element interface
│
├── api/ # High-level fluent API
│ ├── api.go # ModelAPI entry point with namespace access
│ ├── domainmodels.go # EntityBuilder, AssociationBuilder, AttributeBuilder
│ ├── enumerations.go # EnumerationBuilder
│ ├── microflows.go # MicroflowBuilder
│ ├── pages.go # PageBuilder, widget builders
│ └── modules.go # ModulesAPI
│
├── sdk/ # SDK implementation packages
│ ├── domainmodel/ # Entity, Attribute, Association, DomainModel
│ ├── microflows/ # Microflow, Nanoflow, activities (60+ types)
│ ├── pages/ # Page, Layout, Widget types (50+ widgets)
│ ├── widgets/ # Embedded widget templates for pluggable widgets
│ │ ├── loader.go # Template loading with go:embed
│ │ └── templates/ # JSON widget type definitions by Mendix version
│ └── mpr/ # MPR file format handling
│ ├── reader.go # Read-only MPR access
│ ├── writer.go # Read-write MPR modification
│ ├── parser.go # BSON parsing and deserialization
│ └── utils.go # UUID generation utilities
│
├── mdl/ # MDL parser & CLI
│ ├── grammar/ # ANTLR4 grammar definition
│ │ ├── MDLLexer.g4 # Lexer grammar (tokens)
│ │ ├── MDLParser.g4 # Parser grammar (rules)
│ │ └── parser/ # Generated Go parser code
│ ├── ast/ # AST node types for MDL statements
│ ├── visitor/ # ANTLR listener to build AST
│ ├── executor/ # Executes AST against modelsdk-go
│ ├── catalog/ # SQLite-based catalog for querying project metadata
│ ├── linter/ # Extensible linting framework
│ │ └── rules/ # Built-in lint rules
│ └── repl/ # Interactive REPL interface
│
├── sql/ # External database connectivity
│ ├── driver.go # DriverName type, ParseDriver()
│ ├── connection.go # Manager, Connection, credential isolation
│ ├── config.go # DSN resolution (env vars, YAML config)
│ ├── query.go # Execute() -- query via database/sql
│ ├── meta.go # ShowTables(), DescribeTable()
│ ├── format.go # Table and JSON output formatters
│ ├── mendix.go # Mendix DB DSN builder, table/column name helpers
│ ├── import.go # IMPORT pipeline: batch insert, ID generation
│ ├── generate.go # Database Connector MDL generation
│ └── typemap.go # SQL to Mendix type mapping
│
├── cmd/ # Command-line tools
│ ├── mxcli/ # CLI entry point (Cobra-based)
│ └── codegen/ # Code generator CLI
│
├── internal/ # Internal packages (not exported)
│ └── codegen/ # Metamodel code generation system
│ ├── schema/ # JSON reflection data loading
│ ├── transform/ # Transform to Go types
│ └── emit/ # Go source code generation
│
├── generated/metamodel/ # Auto-generated type definitions
├── examples/ # Usage examples
│
├── vscode-mdl/ # VS Code extension (TypeScript, uses bun)
│ ├── src/extension.ts # Extension entry point
│ └── package.json # Extension manifest
│
└── reference/ # Reference materials (not Go code)
├── mendixmodellib/ # TypeScript library + reflection data
├── mendixmodelsdk/ # TypeScript SDK reference
└── mdl-grammar/ # Comprehensive MDL grammar reference
Key Package Responsibilities
| Package | Exported | Description |
|---|---|---|
modelsdk (root) | Yes | Main public API: Open(), OpenForWriting(), helper constructors |
model/ | Yes | Core types: ID, QualifiedName, Module, Element interface |
api/ | Yes | Fluent builder API for domain models, microflows, pages |
sdk/domainmodel/ | Yes | Entity, Attribute, Association Go types |
sdk/microflows/ | Yes | Microflow, Nanoflow, 60+ activity types |
sdk/pages/ | Yes | Page, Layout, 50+ widget types |
sdk/widgets/ | Yes | Widget template loading and cloning |
sdk/mpr/ | Yes | MPR reader, writer, BSON parser |
mdl/grammar/ | Yes | ANTLR4 generated lexer/parser |
mdl/ast/ | Yes | AST node types for all MDL statements |
mdl/visitor/ | Yes | Parse tree to AST conversion |
mdl/executor/ | Yes | AST execution against SDK |
mdl/catalog/ | Yes | SQLite metadata catalog |
mdl/linter/ | Yes | Linting framework and rules |
sql/ | Yes | External database connectivity |
internal/codegen/ | No | Metamodel code generation (not exported) |
Dependencies
| Dependency | Purpose |
|---|---|
modernc.org/sqlite | Pure Go SQLite driver (no CGO required) |
go.mongodb.org/mongo-driver | BSON parsing for Mendix document format |
github.com/antlr4-go/antlr/v4 | ANTLR4 Go runtime for MDL parser |
github.com/spf13/cobra | CLI framework |
github.com/jackc/pgx/v5 | PostgreSQL driver |
github.com/sijms/go-ora/v2 | Oracle driver |
github.com/microsoft/go-mssqldb | SQL Server driver |
Design Decisions
Key architectural decisions made during the development of ModelSDK Go, with rationale and trade-offs.
Pure Go SQLite (No CGO)
Decision: Use modernc.org/sqlite instead of mattn/go-sqlite3.
Rationale: mattn/go-sqlite3 requires CGO and a C compiler, complicating cross-compilation and deployment. The pure Go implementation works on all platforms without external dependencies.
Trade-off: Slightly lower SQLite performance compared to the C implementation. In practice, MPR files are small enough that this is not measurable.
BSON with MongoDB Driver
Decision: Use go.mongodb.org/mongo-driver for BSON parsing.
Rationale: Mendix stores model elements as BSON documents. The MongoDB Go driver provides a mature, well-tested BSON implementation. No actual MongoDB connection is needed – only the BSON codec is used.
SQL-Like DSL (MDL)
Decision: Create a custom SQL-like language instead of using JSON, YAML, or a general-purpose language.
Rationale:
- SQL is familiar to most developers
- LLMs generate SQL-like syntax with high accuracy
- 5-10x more token-efficient than JSON representations
- Human-readable diffs are easier to review than binary or JSON changes
- High information density for scanning microflow logic
Trade-off: Requires building and maintaining a custom parser (ANTLR4).
ANTLR4 for Parsing
Decision: Use ANTLR4 instead of hand-written parser or Go-native parser generators.
Rationale:
- Grammar is shareable across languages (Go, TypeScript, Java, Python)
- Well-defined grammar file serves as the language specification
- Rich tooling for grammar debugging and visualization
- Generated parser handles error recovery automatically
Trade-off: ANTLR4 Go target generates large files. Regeneration requires the ANTLR4 tool (make grammar).
Storage Names in BSON
Decision: Always use storageName (not qualifiedName) for the $Type field in BSON.
Rationale: Studio Pro uses storage names internally. Using qualified names causes TypeCacheUnknownTypeException. The two names are often identical, but critical exceptions exist (e.g., DomainModels$Entity vs DomainModels$EntityImpl).
Inverted Association Pointer Names
Decision: Preserve Mendix’s counter-intuitive naming where ParentPointer points to the FROM entity and ChildPointer points to the TO entity.
Rationale: Changing the field names would create a mapping layer that hides the actual BSON structure, making debugging harder. By keeping the original names, developers can directly compare Go structs with raw BSON output.
Trade-off: Every developer working with associations must learn the inverted semantics. Documentation and comments are essential.
Embedded Widget Templates
Decision: Embed widget templates as JSON files compiled into the binary via go:embed.
Rationale: Pluggable widgets require exact property schemas that change between Mendix versions. Embedding templates ensures the binary is self-contained and version-matched. Extracting templates from Studio Pro guarantees correctness.
Trade-off: New Mendix versions or widgets require extracting and adding new template files.
Catalog as SQLite
Decision: Use an in-memory SQLite database for the project metadata catalog.
Rationale: SQLite provides indexing, FTS5 full-text search, and standard SQL querying without any external dependency. The catalog is rebuilt from the MPR on each session, so persistence is not needed.
Fluent API Layer
Decision: Provide both a low-level SDK (direct struct construction) and a high-level fluent API (api/ package).
Rationale: The low-level SDK gives full control over BSON structure, which is needed for complex operations. The fluent API reduces boilerplate for common tasks and makes test code more readable.
Trade-off: Two API surfaces to maintain and document.
MPR File Format
Mendix projects are stored in .mpr files, which are SQLite databases containing BSON-encoded model elements. This page provides an overview of the format; see v1 vs v2 for version-specific details.
Structure
An MPR file is a standard SQLite database with two key tables:
Unit Table
The Unit table stores document metadata:
| Column | Description |
|---|---|
UnitID | Binary UUID identifying the document |
ContainerID | Parent module UUID |
ContainmentName | Relationship name (e.g., documents) |
UnitType | Fully qualified type name |
Name | Document name |
UnitContents Table (v1 only)
In MPR v1, the UnitContents table stores the actual BSON document content:
| Column | Description |
|---|---|
UnitID | Binary UUID matching the Unit table |
Contents | BSON blob containing the full document |
In MPR v2, document contents are stored as individual files in the mprcontents/ folder instead.
Unit Types
Every document in a Mendix project has a unit type:
| UnitType | Document Type |
|---|---|
DomainModels$DomainModel | Domain model (entities, associations) |
DomainModels$ViewEntitySourceDocument | OQL query for VIEW entities |
Microflows$Microflow | Microflow definition |
Microflows$Nanoflow | Nanoflow definition |
Pages$Page | Page definition |
Pages$Layout | Layout definition |
Pages$Snippet | Snippet definition |
Pages$BuildingBlock | Building block definition |
Enumerations$Enumeration | Enumeration definition |
JavaActions$JavaAction | Java action definition |
Security$ProjectSecurity | Project security settings |
Security$ModuleSecurity | Module security settings |
Navigation$NavigationDocument | Navigation profile |
Settings$ProjectSettings | Project settings |
BusinessEvents$BusinessEventService | Business event service |
BSON Document Structure
Every BSON document contains at minimum:
| Field | Type | Description |
|---|---|---|
$ID | Binary (UUID) | Unique identifier for this element |
$Type | String | Fully qualified type name (using storageName, not qualifiedName) |
ID Format
IDs are stored as BSON Binary subtype 0 (generic) containing UUID bytes:
{
"$ID": {
"Subtype": 0,
"Data": "base64-encoded-uuid"
}
}
Array Convention
Arrays in Mendix BSON have an integer count as the first element:
{
"Attributes": [
3, // Count/type prefix
{ ... }, // First attribute
{ ... } // Second attribute
]
}
The prefix value (typically 2 or 3) indicates the array type. When writing arrays, you must include this prefix. When parsing, skip the first element.
Reference Types
| Reference Type | Storage Format | Example |
|---|---|---|
BY_ID_REFERENCE | Binary UUID | Index AttributePointer |
BY_NAME_REFERENCE | Qualified name string | ValidationRule Attribute |
PART | Embedded BSON object | Child objects serialized inline |
Using the wrong reference format causes Studio Pro to fail loading the model. Always check the metamodel reflection data to determine which format each property uses.
Storage Names vs Qualified Names
The $Type field in BSON must use the storageName, not the qualifiedName. These are often identical but not always:
| qualifiedName (SDK) | storageName (BSON $Type) |
|---|---|
DomainModels$Entity | DomainModels$EntityImpl |
DomainModels$Index | DomainModels$EntityIndex |
Using the wrong name causes TypeCacheUnknownTypeException when opening in Studio Pro. See Storage Names for more details.
v1 (SQLite) vs v2 (mprcontents/)
Mendix uses two MPR file format versions. The library auto-detects and handles both.
MPR v1 (Mendix < 10.18)
A single .mpr SQLite database file containing all model data.
Storage
Unittable: document metadata (name, type, container)UnitContentstable: BSON document blobs
Characteristics
- Self-contained single file
- Larger file size (entire project in one database)
- Binary diffs make Git versioning difficult
- All documents read/written through SQLite queries
Reading
// Auto-detected by modelsdk.Open()
reader, _ := modelsdk.Open("/path/to/project.mpr")
// reader.Version() returns 1
MPR v2 (Mendix >= 10.18)
An .mpr metadata file plus a mprcontents/ folder with individual document files.
Storage
.mprfile: SQLite withUnittable (metadata only, noUnitContents)mprcontents/folder: individual.mxunitfiles containing BSON
Each .mxunit file is named by the document’s UUID and contains the raw BSON for that document.
Directory Layout
project/
├── app.mpr # SQLite metadata
└── mprcontents/
├── 2a3b4c5d-... # Domain model BSON
├── 6e7f8a9b-... # Microflow BSON
├── c0d1e2f3-... # Page BSON
└── ...
Characteristics
- Better for Git versioning – individual files change independently
- Smaller diffs when modifying a single document
- Parallel read access to different documents
- Requires both the
.mprfile and themprcontents/folder
Reading
// Auto-detected by modelsdk.Open()
reader, _ := modelsdk.Open("/path/to/project.mpr")
// reader.Version() returns 2
Format Detection
The library detects the format by checking whether the UnitContents table exists in the SQLite database:
| Condition | Format |
|---|---|
UnitContents table exists and has rows | v1 |
UnitContents table missing or empty, mprcontents/ folder exists | v2 |
This detection is automatic – callers of Open() and OpenForWriting() do not need to specify the format.
Comparison
| Feature | v1 | v2 |
|---|---|---|
| Mendix version | < 10.18 | >= 10.18 |
| File structure | Single .mpr | .mpr + mprcontents/ |
| Document storage | SQLite UnitContents table | Individual .mxunit files |
| Git friendliness | Poor (binary diffs) | Good (per-document files) |
| File size | Larger single file | Distributed across files |
| Read performance | Single DB query | File I/O per document |
| Write granularity | Full DB transaction | Per-file write |
Writing Behavior
When writing with OpenForWriting():
- v1: Documents are written as BSON blobs into the
UnitContentstable within a SQLite transaction - v2: Documents are written as individual
.mxunitfiles in themprcontents/folder; theUnittable metadata is updated in SQLite
The writer handles both formats transparently.
BSON Document Structure
How Mendix project documents are serialized in BSON format, including the role of $Type fields, nested structures, array version markers, and widget serialization.
Source of Truth
The authoritative reference for BSON serialization is the reflection-data at:
reference/mendixmodellib/reflection-data/{version}-structures.json
Each structure entry contains:
qualifiedName: The API name (e.g.,Pages$DivContainer)storageName: The BSON$Typevalue (e.g.,Forms$DivContainer)defaultSettings: Required default property valuesproperties: Property definitions with types and requirements
Type Name Mapping
Mendix uses different prefixes for API names vs storage names:
| API Prefix | Storage Prefix | Domain |
|---|---|---|
Pages$ | Forms$ | Page widgets |
Microflows$ | Microflows$ | Microflow elements |
DomainModels$ | DomainModels$ | Domain model elements |
Texts$ | Texts$ | Text/translation elements |
DataTypes$ | DataTypes$ | Data type definitions |
CustomWidgets$ | CustomWidgets$ | Pluggable widgets |
Common Type Name Mistakes
| Incorrect (will fail) | Correct Storage Name |
|---|---|
Forms$NoClientAction | Forms$NoAction |
Forms$PageClientAction | Forms$FormAction |
Forms$MicroflowClientAction | Forms$MicroflowAction |
Pages$DivContainer | Forms$DivContainer |
Pages$ActionButton | Forms$ActionButton |
Unit Document Structure
Each model element (entity, microflow, page, etc.) is stored as a BSON document:
graph LR
subgraph "BSON Document"
ID["$ID: binary UUID"]
TYPE["$Type: 'DomainModels$Entity'"]
NAME["Name: 'Customer'"]
ATTRS["Attributes: [...]"]
LOC["Location: '100;200'"]
end
Array Version Markers
Mendix BSON uses version markers for arrays:
| Marker | Meaning |
|---|---|
[3] | Empty array |
[2, item1, item2, ...] | Non-empty array with items |
[3, item1, item2, ...] | Non-empty array (text items) |
Example:
"Widgets": [3] // Empty widgets array
"Widgets": [2, {...}] // One widget
"Items": [3, {...}] // One text translation item
Important: These version markers only exist in BSON format, not in JSON templates. In JSON, [2] is an array containing the integer 2, not an empty array with a version marker.
Page Structure
A page document has this top-level structure:
{
"$ID": "<uuid>",
"$Type": "Forms$Page",
"AllowedModuleRoles": [1],
"Appearance": { ... },
"Autofocus": "DesktopOnly",
"CanvasHeight": 600,
"CanvasWidth": 1200,
"Documentation": "",
"Excluded": false,
"ExportLevel": "Hidden",
"FormCall": { ... },
"Name": "PageName",
"Parameters": [3, ...],
"PopupCloseAction": "",
"Title": { ... },
"Url": "page_url",
"Variables": [3]
}
Appearance Object
Standard appearance object for widgets:
{
"$ID": "<uuid>",
"$Type": "Forms$Appearance",
"Class": "",
"DesignProperties": [3],
"DynamicClasses": "",
"Style": ""
}
Widget Default Properties
Each widget type requires specific default properties to be serialized. Studio Pro will fail to load the project if required properties are missing.
DivContainer (Container)
{
"$Type": "Forms$DivContainer",
"Appearance": { ... },
"ConditionalVisibilitySettings": null,
"Name": "",
"NativeAccessibilitySettings": null,
"OnClickAction": { "$Type": "Forms$NoAction", ... },
"RenderMode": "Div",
"ScreenReaderHidden": false,
"TabIndex": 0,
"Widgets": [3]
}
DataView
{
"$Type": "Forms$DataView",
"Appearance": { ... },
"ConditionalEditabilitySettings": null,
"ConditionalVisibilitySettings": null,
"DataSource": { ... },
"Editability": "Always",
"FooterWidgets": [3],
"LabelWidth": 3,
"Name": "",
"NoEntityMessage": { ... },
"ReadOnlyStyle": "Control",
"ShowFooter": true,
"TabIndex": 0,
"Widgets": [3]
}
Input Widgets (TextBox, TextArea, DatePicker, etc.)
Input widgets require several non-null properties:
{
"$Type": "Forms$TextBox",
"Appearance": { ... },
"AriaRequired": false,
"AttributeRef": {
"$ID": "<uuid>",
"$Type": "DomainModels$AttributeRef",
"Attribute": "Module.Entity.AttributeName",
"EntityRef": null
},
"ConditionalEditabilitySettings": null,
"ConditionalVisibilitySettings": null,
"Editable": "Always",
"FormattingInfo": { ... },
"IsPasswordBox": false,
"LabelTemplate": { ... },
"Name": "textBox1",
"NativeAccessibilitySettings": null,
"OnChangeAction": { "$Type": "Forms$NoAction", ... },
"OnEnterAction": { "$Type": "Forms$NoAction", ... },
"OnEnterKeyPressAction": { "$Type": "Forms$NoAction", ... },
"OnLeaveAction": { "$Type": "Forms$NoAction", ... },
"PlaceholderTemplate": { ... },
"ReadOnlyStyle": "Inherit",
"TabIndex": 0,
"Validation": { ... }
}
Required nested objects:
AttributeRef– Must haveAttributeas fully qualified path (e.g.,Module.Entity.AttributeName)FormattingInfo– Required for TextBox and DatePickerPlaceholderTemplate– RequiredForms$ClientTemplateobjectValidation– RequiredForms$WidgetValidationobject
Attribute Path Resolution
The AttributeRef.Attribute field requires a fully qualified path in the format Module.Entity.AttributeName. When using short attribute names in MDL, the SDK automatically resolves them using the DataView’s entity context.
Client Action Types
| Action Type | Storage Name |
|---|---|
| No Action | Forms$NoAction |
| Save Changes | Forms$SaveChangesClientAction |
| Cancel Changes | Forms$CancelChangesClientAction |
| Close Page | Forms$ClosePageClientAction |
| Delete | Forms$DeleteClientAction |
| Show Page | Forms$FormAction |
| Call Microflow | Forms$MicroflowAction |
| Call Nanoflow | Forms$CallNanoflowClientAction |
Pluggable Widgets (CustomWidget)
Pluggable widgets like ComboBox use CustomWidgets$CustomWidget with a complex nested structure:
{
"$Type": "CustomWidgets$CustomWidget",
"Appearance": { ... },
"Name": "comboBox1",
"Object": {
"$Type": "CustomWidgets$WidgetObject",
"Properties": [2, { ... }],
"TypePointer": "<binary ID referencing ObjectType>"
},
"Type": {
"$Type": "CustomWidgets$CustomWidgetType",
"ObjectType": {
"$Type": "CustomWidgets$WidgetObjectType",
"PropertyTypes": [2, { ... }]
},
"WidgetId": "com.mendix.widget.web.combobox.Combobox"
}
}
TypePointer References
There are three levels of TypePointer references:
- WidgetObject.TypePointer references
ObjectType.$ID(the WidgetObjectType) - WidgetProperty.TypePointer references
PropertyType.$ID(the WidgetPropertyType) - WidgetValue.TypePointer references
ValueType.$ID(the WidgetValueType)
CustomWidgetType
└── ObjectType (WidgetObjectType) ← WidgetObject.TypePointer
└── PropertyTypes[]
└── PropertyType (WidgetPropertyType) ← WidgetProperty.TypePointer
└── ValueType (WidgetValueType) ← WidgetValue.TypePointer
If any TypePointer is missing or references an invalid ID, Studio Pro will fail to load the widget.
Widget Size
Each pluggable widget instance contains a full copy of both Type and Object:
| Component | BSON Size | Description |
|---|---|---|
| Type (CustomWidgetType) | ~54 KB | Widget definition with all PropertyTypes |
| Object (WidgetObject) | ~34 KB | Property values for all PropertyTypes |
| Total per widget | ~88 KB |
There is no deduplication within a page. A page with 4 ComboBox widgets requires ~352 KB for widgets alone.
Common Errors
| Error | Cause | Solution |
|---|---|---|
| “The type cache does not contain a type with qualified name X” | Incorrect $Type value | Check reflection-data for correct storage name |
| “No entity configured for the data source” | Missing or incorrect DataView DataSource | Configure Forms$DataViewSource with proper EntityRef |
| CE0463 “widget definition has changed” | Object properties don’t match Type PropertyTypes | Use template Object as base, only modify needed properties |
| “Project uses features that are no longer supported” | Missing widget default properties | Include all required defaults from reflection-data |
Files Reference
| File | Purpose |
|---|---|
sdk/mpr/writer_widgets.go | Widget serialization to BSON |
sdk/mpr/writer_pages.go | Page serialization |
sdk/mpr/reader_widgets.go | Widget template extraction and cloning |
sdk/mpr/parser_page.go | Page deserialization |
sdk/widgets/loader.go | Embedded template loading |
sdk/widgets/templates/mendix-11.6/*.json | Embedded widget templates |
reference/mendixmodellib/reflection-data/*.json | Type definitions |
Storage Names vs Qualified Names
Mendix uses different “storage names” in BSON $Type fields than the “qualified names” shown in the TypeScript SDK documentation. Using the wrong name causes a TypeCacheUnknownTypeException when opening the project in Studio Pro.
The Problem
When serializing model elements to BSON, the $Type field must use the storage name, not the qualified name from the SDK documentation. These names differ in ways that are not always predictable.
Domain Prefix Mapping
The most systematic difference is the domain prefix used for page-related types:
| API Prefix | Storage Prefix | Domain |
|---|---|---|
Pages$ | Forms$ | Page widgets |
Microflows$ | Microflows$ | Microflow elements (same) |
DomainModels$ | DomainModels$ | Domain model elements (same) |
Texts$ | Texts$ | Text/translation elements (same) |
DataTypes$ | DataTypes$ | Data type definitions (same) |
CustomWidgets$ | CustomWidgets$ | Pluggable widgets (same) |
The Pages$ to Forms$ renaming reflects Mendix’s historical terminology – “Form” was the original term for “Page”.
Microflow Action Name Mapping
Several microflow actions have storage names that differ significantly from their qualified names:
| Qualified Name (SDK/docs) | Storage Name (BSON $Type) | Note |
|---|---|---|
CreateObjectAction | CreateChangeAction | |
ChangeObjectAction | ChangeAction | |
DeleteObjectAction | DeleteAction | |
CommitObjectsAction | CommitAction | |
RollbackObjectAction | RollbackAction | |
AggregateListAction | AggregateAction | |
ListOperationAction | ListOperationsAction | Note the plural |
ShowPageAction | ShowFormAction | “Form” was original for “Page” |
ClosePageAction | CloseFormAction | “Form” was original for “Page” |
Client Action Name Mapping
Page client actions also have storage name differences:
| Incorrect (will fail) | Correct Storage Name |
|---|---|
Forms$NoClientAction | Forms$NoAction |
Forms$PageClientAction | Forms$FormAction |
Forms$MicroflowClientAction | Forms$MicroflowAction |
Pages$DivContainer | Forms$DivContainer |
Pages$ActionButton | Forms$ActionButton |
How to Verify Storage Names
When adding new types, always verify the storage name by:
- Examining existing MPR files with the
mxtool or an SQLite browser - Checking the reflection data in
reference/mendixmodellib/reflection-data/ - Looking at the parser cases in
sdk/mpr/parser_microflow.go
Querying Reflection Data
Use this Python snippet to check widget default settings:
import json
with open('reference/mendixmodellib/reflection-data/11.0.0-structures.json') as f:
data = json.load(f)
# Find widget by API name
widget = data.get('Pages$DivContainer', {})
print('Storage name:', widget.get('storageName'))
print('Defaults:', json.dumps(widget.get('defaultSettings', {}), indent=2))
# Search by storage name
for key, val in data.items():
if val.get('storageName') == 'Forms$NoAction':
print(f'{key}: {val.get("defaultSettings")}')
Association Parent/Child Pointer Semantics
Mendix BSON uses inverted naming for association pointers, which is counter-intuitive:
| BSON Field | Points To | MDL Keyword |
|---|---|---|
ParentPointer | FROM entity (FK owner) | FROM Module.Child |
ChildPointer | TO entity (referenced) | TO Module.Parent |
For example, CREATE ASSOCIATION Mod.Child_Parent FROM Mod.Child TO Mod.Parent stores:
ParentPointer = Child.$ID(the FROM entity owns the foreign key)ChildPointer = Parent.$ID(the TO entity is being referenced)
This affects entity access rules: MemberAccess entries for associations must only be added to the FROM entity (the one stored in ParentPointer). Adding them to the TO entity triggers CE0066 “Entity access is out of date”.
Key Takeaway
When unsure about the correct BSON structure for a new feature, create a working example in Mendix Studio Pro and compare the generated BSON against a known-good reference. The reflection data at reference/mendixmodellib/reflection-data/ is the definitive source for storage names and default values.
Widget Template System
Pluggable widgets (DataGrid2, ComboBox, Gallery, etc.) require embedded template definitions for correct BSON serialization. This page explains how the template system works.
Why Templates Are Needed
Pluggable widgets in Mendix are defined by two components:
- Type (
PropertyTypesschema) – defines what properties the widget accepts - Object (
WidgetObjectdefaults) – provides default values for all properties
Both must be present in the BSON output. The type defines the schema; the object provides a valid instance. If the object’s property structure does not match the type’s schema, Studio Pro reports CE0463 “widget definition changed”.
Template Location
Templates are embedded in the binary via Go’s go:embed directive:
sdk/widgets/
├── loader.go # Template loading with go:embed
└── templates/
├── README.md # Template extraction requirements
├── 10.6.0/ # Templates by Mendix version
│ ├── DataGrid2.json
│ ├── ComboBox.json
│ ├── Gallery.json
│ └── ...
└── 11.6.0/
├── DataGrid2.json
└── ...
Each JSON file contains both the type and object fields for a specific widget at a specific Mendix version.
Template Structure
A template JSON file looks like:
{
"type": {
"$Type": "CustomWidgets$WidgetPropertyTypes",
"Properties": [
{ "Name": "columns", "Type": "Object", ... },
{ "Name": "showLabel", "Type": "Boolean", ... }
]
},
"object": {
"$Type": "CustomWidgets$WidgetObject",
"Properties": [
{ "Name": "columns", "Value": [] },
{ "Name": "showLabel", "Value": true }
]
}
}
Extracting Templates
Templates must be extracted from Studio Pro-created widgets, not generated programmatically. The extraction process:
- Create a widget instance in Studio Pro
- Save the project
- Extract the BSON from the
.mprfile - Convert the
typeandobjectsections to JSON - Save as a template file
This ensures the property structure exactly matches what Studio Pro expects.
Important: Programmatically generated templates often have subtle differences in property ordering, default values, or nested structures that cause CE0463 errors. Always extract from Studio Pro.
Loading Templates at Runtime
The loader.go file provides functions to load templates:
// Load a widget template for the project's Mendix version
template, err := widgets.LoadTemplate("DataGrid2", mendixVersion)
The loader searches for the most specific version match, falling back to earlier versions if an exact match is not available.
JSON Templates vs BSON Serialization
When editing template JSON files, use standard JSON conventions:
- Empty arrays are
[](not[3]) - Booleans are
true/false - Strings are
"quoted"
The array count prefixes ([3, ...]) required by Mendix BSON are added automatically during serialization. Writing [2] in a JSON template creates an array containing the integer 2, not an empty BSON array.
Debugging CE0463 Errors
If a page fails with CE0463 after creation:
- Create the same widget manually in Studio Pro
- Extract its BSON from the saved project
- Compare the template’s
objectproperties against the Studio Pro version - Look for missing properties, wrong default values, or incorrect nesting
- Update the template JSON to match
See the debug workflow in .claude/skills/debug-bson.md for detailed steps.
MDL Parser
The MDL parser translates SQL-like MDL (Mendix Definition Language) syntax into executable operations against Mendix project files. It uses ANTLR4 for grammar definition, enabling cross-language grammar sharing with other implementations (TypeScript, Java, Python).
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ MDL Input String │
│ "SHOW ENTITIES IN MyModule" │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ANTLR4 Lexer (mdl_lexer.go) │
│ Generated from MDLLexer.g4 - Tokenizes input into SHOW, ENTITIES,│
│ IN, IDENTIFIER tokens │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ANTLR4 Parser (mdl_parser.go) │
│ Generated from MDLParser.g4 - Builds parse tree according to │
│ grammar rules │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ ANTLR Listener (visitor/visitor.go) │
│ Walks parse tree and builds strongly-typed AST nodes │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AST (ast/ast.go) │
│ *ast.ShowStmt{Type: "ENTITIES", Module: "MyModule"} │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Executor (executor/executor.go) │
│ Executes AST against modelsdk-go API │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ modelsdk-go Library │
│ mpr.Writer, domainmodel.Entity, etc. │
└─────────────────────────────────────────────────────────────────────┘
Directory Structure
mdl/
├── grammar/
│ ├── MDLLexer.g4 # ANTLR4 lexer grammar (tokens)
│ ├── MDLParser.g4 # ANTLR4 parser grammar (rules)
│ └── parser/ # Generated parser code (DO NOT EDIT)
│ ├── mdl_lexer.go
│ ├── mdl_parser.go
│ ├── mdlparser_listener.go
│ └── mdlparser_base_listener.go
├── ast/
│ └── ast.go, ast_microflow.go, ast_expression.go, ast_datatype.go, ...
├── visitor/
│ └── visitor.go # ANTLR listener implementation
├── executor/
│ ├── executor.go # AST execution logic
│ ├── cmd_microflows_builder.go # Microflow builder (variable tracking)
│ └── validate_microflow.go # AST-level semantic checks (mxcli check)
├── catalog/
│ └── catalog.go # SQLite-based project metadata catalog
├── linter/
│ ├── linter.go # Linting framework
│ └── rules/ # Built-in lint rules
└── repl/
└── repl.go # Interactive REPL interface
Why ANTLR4?
| Consideration | ANTLR4 | Parser Combinators |
|---|---|---|
| Cross-language | Same grammar for Go, TS, Java | Rewrite per language |
| Grammar docs | EBNF-like, readable | Code is the doc |
| Error messages | Built-in recovery | Custom implementation |
| Performance | Optimized lexer/parser | Comparable |
| Tooling | ANTLR Lab, IDE plugins | Limited |
Why Listener Pattern (not Visitor)?
MDL uses the listener pattern rather than the visitor pattern:
- Listener: Callbacks fired during tree walk, simpler for AST building
- Visitor: Returns values from each node, better for expression evaluation
For MDL, statements are independent and do not need return value propagation, making the listener pattern more appropriate.
Regenerating the Parser
After modifying MDLLexer.g4 or MDLParser.g4:
make grammar
Or manually:
cd mdl/grammar
antlr4 -Dlanguage=Go -package parser -o parser MDLLexer.g4 MDLParser.g4
Requirements:
- ANTLR4 tool (
antlr4command or Java JAR) - Go target runtime (
github.com/antlr4-go/antlr/v4)
ANTLR4 Grammar Design
The MDL parser uses ANTLR4 with separate lexer and parser grammars. This page covers the design patterns and key decisions in the grammar.
Grammar Files
| File | Purpose |
|---|---|
mdl/grammar/MDLLexer.g4 | Token definitions (keywords, operators, literals) |
mdl/grammar/MDLParser.g4 | Parser rules (statements, expressions, blocks) |
mdl/grammar/parser/ | Generated Go code (do not edit) |
Regenerating the Parser
After modifying either .g4 file:
make grammar
This runs the ANTLR4 tool to regenerate the Go parser code in mdl/grammar/parser/.
Key Design Patterns
Case-Insensitive Keywords
MDL keywords are case-insensitive. This is implemented using fragment rules in the lexer:
// Lexer fragments for case-insensitive matching
fragment A : [aA] ;
fragment B : [bB] ;
// ...
// Keywords use fragments
SHOW : S H O W ;
CREATE : C R E A T E ;
ENTITY : E N T I T Y ;
This means SHOW, show, Show, and sHoW are all valid.
Separate Lexer and Parser
The grammar uses split lexer/parser grammars (not a combined grammar). This provides:
- Clearer separation between tokenization and parsing
- Better control over token modes
- Easier maintenance of complex grammars
Statement Structure
Top-level statements follow a consistent pattern:
statement
: showStatement
| describeStatement
| createStatement
| alterStatement
| dropStatement
| grantStatement
| revokeStatement
| searchStatement
| refreshStatement
| sqlStatement
| importStatement
// ...
;
Each statement type has its own rule, keeping the grammar modular.
Qualified Names
Module-qualified names (Module.Entity) are a fundamental pattern:
qualifiedName
: IDENTIFIER ('.' IDENTIFIER)*
;
This matches Customer, Sales.Customer, and Sales.Customer.Name.
Widget Trees
Page definitions use recursive widget nesting with curly braces:
widgetDecl
: widgetType IDENTIFIER widgetProperties? widgetBody?
;
widgetBody
: '{' widgetDecl* '}'
;
This allows arbitrary nesting depth for layout grids, containers, and data views.
Architecture Pipeline
MDL Input String
│
▼
┌──────────────┐
│ ANTLR4 Lexer │ Tokenizes into SHOW, IDENTIFIER, etc.
└──────┬───────┘
│
▼
┌──────────────┐
│ ANTLR4 Parser│ Builds parse tree from grammar rules
└──────┬───────┘
│
▼
┌──────────────┐
│ Visitor │ Walks parse tree, builds typed AST nodes
│ (Listener) │ (mdl/visitor/visitor.go)
└──────┬───────┘
│
▼
┌──────────────┐
│ AST │ Strongly-typed statement nodes
│ │ (mdl/ast/ast.go)
└──────┬───────┘
│
▼
┌──────────────┐
│ Executor │ Executes AST against modelsdk-go
│ │ (mdl/executor/executor.go)
└──────────────┘
Visitor (Listener) Pattern
ANTLR4 generates a listener interface. The MDL visitor (mdl/visitor/visitor.go) implements this interface to build AST nodes:
EnterCreateEntityStatement-> creates*ast.CreateEntityStmtEnterCreateMicroflowStatement-> creates*ast.CreateMicroflowStmtEnterShowStatement-> creates*ast.ShowStmt
Each listener method extracts data from the parse tree context and constructs the corresponding AST node.
Error Recovery
The ANTLR4 parser provides built-in error recovery. When a syntax error is encountered:
- The parser reports the error with line/column position
- It attempts to recover by consuming or inserting tokens
- Parsing continues to find additional errors
This enables the language server to report multiple syntax errors in a single pass.
Adding New Syntax
See Adding New Statements for the step-by-step process of extending the grammar.
Lexer - Parser - AST - Executor Pipeline
The complete parsing pipeline from raw MDL text through lexical analysis, parsing, AST construction, and execution against the model.
Pipeline Flow
flowchart TD
INPUT[/"SHOW ENTITIES IN MyModule"/]
subgraph Parsing
LEXER[ANTLR4 Lexer]
PARSER[ANTLR4 Parser]
TREE[Parse Tree]
end
subgraph "AST Building"
VISITOR[AST Visitor]
AST[ShowStmt AST Node]
end
subgraph Execution
EXEC[Executor]
SDK[SDK API Call]
FORMAT[Format Output]
end
OUTPUT[/Markdown Table/]
INPUT --> LEXER
LEXER --> PARSER
PARSER --> TREE
TREE --> VISITOR
VISITOR --> AST
AST --> EXEC
EXEC --> SDK
SDK --> FORMAT
FORMAT --> OUTPUT
Stage 1: ANTLR4 Grammar
The grammar defines MDL syntax using ANTLR4’s EBNF-like notation.
Case-Insensitive Keywords
Uses fragment rules for case-insensitive matching:
// Keywords are case-insensitive
SHOW : S H O W ;
ENTITY : E N T I T Y ;
// Fragment rules for each letter
fragment S : [sS] ;
fragment H : [hH] ;
fragment O : [oO] ;
fragment W : [wW] ;
// ... etc
This allows SHOW, show, Show, etc. to all match the same token.
Labeled Alternatives
Parser rules use labeled alternatives for type-safe listener methods:
showStatement
: SHOW MODULES SEMI? # ShowModules
| SHOW ENTITIES (IN IDENTIFIER)? SEMI? # ShowEntities
| SHOW ENTITY qualifiedName SEMI? # ShowEntity
;
Each label generates a specific listener method (e.g., EnterShowModules, EnterShowEntities).
Whitespace Handling
Whitespace is sent to a hidden channel (skipped):
WS : [ \t\r\n]+ -> skip ;
Stage 2: Generated Parser
ANTLR4 generates four files from the grammar:
| File | Purpose |
|---|---|
mdl_lexer.go | Tokenizer – converts input to token stream |
mdl_parser.go | Parser – builds parse tree from tokens |
mdl_listener.go | Listener interface – callbacks for each rule |
mdl_base_listener.go | Empty listener implementation for extension |
Stage 3: AST Types
Strongly-typed AST nodes representing MDL statements:
// Statement is the interface for all MDL statements
type Statement interface {
statementNode()
}
// ShowStmt represents SHOW commands
type ShowStmt struct {
Type string // MODULES, ENTITIES, ASSOCIATIONS, ENUMERATIONS
Module string // Optional: filter by module
Name QualifiedName // For SHOW ENTITY/ASSOCIATION
}
// CreateEntityStmt represents CREATE ENTITY
type CreateEntityStmt struct {
Name QualifiedName
Persistent bool
Attributes []Attribute
Position *Position
Comment string
Doc string
}
// QualifiedName represents Module.Name or just Name
type QualifiedName struct {
Module string
Name string
}
Stage 4: ANTLR Listener (Visitor)
The visitor walks the ANTLR parse tree and builds AST nodes.
Type Assertions for Context Access
func (v *Visitor) EnterShowEntities(ctx *parser.ShowEntitiesContext) {
stmt := &ast.ShowStmt{Type: "ENTITIES"}
// Access IDENTIFIER token if present (IN clause)
if id := ctx.IDENTIFIER(); id != nil {
stmt.Module = id.GetText()
}
v.program.Statements = append(v.program.Statements, stmt)
}
Building Qualified Names
func buildQualifiedName(ctx parser.IQualifiedNameContext) ast.QualifiedName {
qn := ctx.(*parser.QualifiedNameContext)
ids := qn.AllIDENTIFIER()
if len(ids) == 1 {
return ast.QualifiedName{Name: ids[0].GetText()}
}
return ast.QualifiedName{
Module: ids[0].GetText(),
Name: ids[1].GetText(),
}
}
Error Handling
Syntax errors are collected via a custom error listener:
type ErrorListener struct {
*antlr.DefaultErrorListener
Errors []error
}
func (e *ErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{},
line, column int, msg string, ex antlr.RecognitionException) {
e.Errors = append(e.Errors, fmt.Errorf("line %d:%d %s", line, column, msg))
}
Stage 5: Executor
Executes AST statements against the modelsdk-go API:
type Executor struct {
writer *mpr.Writer
output io.Writer
}
func (e *Executor) Execute(stmt ast.Statement) error {
switch s := stmt.(type) {
case *ast.ConnectStmt:
return e.executeConnect(s)
case *ast.ShowStmt:
return e.executeShow(s)
case *ast.CreateEntityStmt:
return e.executeCreateEntity(s)
// ... other statement types
}
}
Integration with SDK
func (e *Executor) executeCreateEntity(stmt *ast.CreateEntityStmt) error {
// Build domain model entity
entity := &domainmodel.Entity{
ID: mpr.GenerateID(),
Name: stmt.Name.Name,
// ... other fields
}
// Get module and add entity
module := e.getOrCreateModule(stmt.Name.Module)
dm := module.DomainModel
dm.Entities = append(dm.Entities, entity)
return nil
}
Stage 6: REPL
Interactive read-eval-print loop ties the pipeline together:
type REPL struct {
executor *executor.Executor
input io.Reader
output io.Writer
}
func (r *REPL) Run() error {
scanner := bufio.NewScanner(r.input)
for {
fmt.Fprint(r.output, "mdl> ")
if !scanner.Scan() {
break
}
input := scanner.Text()
prog, errs := visitor.Build(input)
if len(errs) > 0 {
// Handle parse errors
continue
}
for _, stmt := range prog.Statements {
if err := r.executor.Execute(stmt); err != nil {
fmt.Fprintf(r.output, "Error: %v\n", err)
}
}
}
return nil
}
Extending the Parser
Adding a New Statement Type
- Update grammar (
MDLLexer.g4for tokens,MDLParser.g4for rules):
ddlStatement
: createStatement
| newStatement // Add new statement
;
newStatement
: NEW KEYWORD qualifiedName SEMI? # NewKeyword
;
NEW : N E W ;
KEYWORD : K E Y W O R D ;
- Regenerate parser:
make grammar
- Add AST type (
ast/ast.go):
type NewKeywordStmt struct {
Name QualifiedName
}
func (*NewKeywordStmt) statementNode() {}
- Update visitor (
visitor/visitor.go):
func (v *Visitor) EnterNewKeyword(ctx *parser.NewKeywordContext) {
stmt := &ast.NewKeywordStmt{
Name: buildQualifiedName(ctx.QualifiedName()),
}
v.program.Statements = append(v.program.Statements, stmt)
}
- Update executor (
executor/executor.go):
func (e *Executor) Execute(stmt ast.Statement) error {
switch s := stmt.(type) {
// ... existing cases
case *ast.NewKeywordStmt:
return e.executeNewKeyword(s)
}
}
Microflow Validation
Before execution, mxcli check runs AST-level semantic checks on microflow bodies via ValidateMicroflow(). These checks operate purely on the parsed AST and require no project connection:
- Return value consistency – RETURN must provide a value when the microflow declares a return type
- Return type plausibility – Scalar literals cannot be returned from entity-typed microflows
- Return path coverage – All code paths must end with RETURN for non-void microflows
- Variable scope – Variables declared inside IF/ELSE branches cannot be referenced after the branch ends
- Validation feedback – VALIDATION FEEDBACK must have a non-empty message template
Handling Nil Values from ANTLR
ANTLR parsers can return partial parse trees with nil nodes when there are syntax errors. Always check if grammar element getters return nil before calling methods on them:
// DANGEROUS - will panic if AttributeName() returns nil
attr.Name = a.AttributeName().GetText()
// SAFE - check for nil first
if a.AttributeName() == nil {
b.addErrorWithExample(
"Invalid attribute: each attribute must have a name and type",
` CREATE PERSISTENT ENTITY MyModule.Customer (
Name: String(100) NOT NULL,
Email: String(200),
Age: Integer
);`)
continue
}
attr.Name = a.AttributeName().GetText()
Common ANTLR context methods that can return nil on parse errors:
AttributeName()– missing attribute identifierEnumValueName()– missing enumeration value identifierQualifiedName()– missing or malformed qualified nameDataType()– missing type specificationExpression()– missing or malformed expression
Adding New Statements
Step-by-step guide for adding a new MDL statement to the parser and executor.
Overview
Adding a new statement requires changes in five layers:
- Grammar – lexer tokens and parser rules
- AST – typed node struct
- Visitor – parse tree to AST conversion
- Executor – AST to SDK call
- Tests – parser and execution tests
Step 1: Grammar Rules
Add Tokens (if needed)
In mdl/grammar/MDLLexer.g4, add any new keywords:
WORKFLOW : W O R K F L O W ;
Add Parser Rule
In mdl/grammar/MDLParser.g4, add the statement rule:
// Add to the statement alternatives
statement
: ...
| createWorkflowStatement
;
// Define the new statement
createWorkflowStatement
: CREATE WORKFLOW qualifiedName
'(' workflowBody ')'
;
workflowBody
: workflowActivity (workflowActivity)*
;
workflowActivity
: USER_TASK IDENTIFIER STRING_LITERAL
;
Regenerate Parser
make grammar
Step 2: AST Node
In mdl/ast/, create or extend an AST file:
// ast/ast_workflow.go
package ast
type CreateWorkflowStmt struct {
Name QualifiedName
Activities []*WorkflowActivity
}
type WorkflowActivity struct {
Type string
Name string
Label string
}
Step 3: Visitor Implementation
In mdl/visitor/visitor.go, implement the listener method:
func (v *MDLVisitor) EnterCreateWorkflowStatement(ctx *parser.CreateWorkflowStatementContext) {
stmt := &ast.CreateWorkflowStmt{
Name: v.visitQualifiedName(ctx.QualifiedName()),
}
for _, actCtx := range ctx.AllWorkflowActivity() {
activity := &ast.WorkflowActivity{
Type: "UserTask",
Name: actCtx.IDENTIFIER().GetText(),
Label: unquote(actCtx.STRING_LITERAL().GetText()),
}
stmt.Activities = append(stmt.Activities, activity)
}
v.statements = append(v.statements, stmt)
}
Step 4: Executor Handler
In mdl/executor/, create the execution handler:
// executor/cmd_workflows.go
func (e *Executor) execCreateWorkflow(stmt *ast.CreateWorkflowStmt) error {
module, err := e.findModule(stmt.Name.Module)
if err != nil {
return err
}
// Build the workflow using SDK types
workflow := &workflows.Workflow{
Name: stmt.Name.Name,
// ... populate from stmt
}
return e.writer.CreateWorkflow(module.ID, workflow)
}
Register the handler in executor.go:
func (e *Executor) Execute(stmt ast.Statement) error {
switch s := stmt.(type) {
// ... existing cases
case *ast.CreateWorkflowStmt:
return e.execCreateWorkflow(s)
}
}
Step 5: Tests
Parser Test
Verify the grammar parses correctly:
func TestParseCreateWorkflow(t *testing.T) {
input := `CREATE WORKFLOW Sales.ApprovalProcess (
USER_TASK reviewTask 'Review Order'
);`
stmts, err := parser.Parse(input)
require.NoError(t, err)
require.Len(t, stmts, 1)
ws := stmts[0].(*ast.CreateWorkflowStmt)
assert.Equal(t, "Sales", ws.Name.Module)
assert.Equal(t, "ApprovalProcess", ws.Name.Name)
}
Execution Test
Test against a real MPR file:
func TestExecuteCreateWorkflow(t *testing.T) {
writer := setupTestProject(t)
defer writer.Close()
err := executor.ExecuteString(writer,
`CREATE WORKFLOW TestModule.MyWorkflow (...);`)
require.NoError(t, err)
}
Checklist
- Lexer tokens added (if new keywords)
- Parser rule added
- Parser regenerated (
make grammar) - AST node defined
- Visitor listener implemented
- Executor handler registered
- Parser tests pass
- Execution tests pass
-
mxcli checkvalidates the new syntax - Studio Pro opens the modified project without errors
Catalog System
The catalog is an in-memory SQLite database that indexes Mendix project metadata for fast querying, full-text search, and cross-reference navigation.
Purpose
Reading a Mendix project requires parsing BSON documents from the MPR file. The catalog pre-processes this data into relational tables, enabling:
- SQL queries against project metadata (
SELECT ... FROM CATALOG.ENTITIES) - Full-text search across all strings and source (
SEARCH 'keyword') - Cross-reference tracking (callers, callees, references, impact analysis)
- Lint rule evaluation (Starlark rules query catalog tables)
Building the Catalog
The catalog is populated by the REFRESH CATALOG command:
-- Quick refresh: basic tables (modules, entities, microflows, pages, etc.)
REFRESH CATALOG;
-- Full refresh: adds cross-references, widgets, and source tables
REFRESH CATALOG FULL;
The full refresh is required for:
SHOW CALLERS/SHOW CALLEES/SHOW REFERENCES/SHOW IMPACTSHOW WIDGETS/UPDATE WIDGETSSELECT ... FROM CATALOG.REFSSELECT ... FROM CATALOG.SOURCE
Querying the Catalog
Use standard SQL syntax with the CATALOG. prefix:
-- Find large microflows
SELECT Name, ActivityCount
FROM CATALOG.MICROFLOWS
WHERE ActivityCount > 10
ORDER BY ActivityCount DESC;
-- Find all entity usages
SELECT SourceName, RefKind, TargetName
FROM CATALOG.REFS
WHERE TargetName = 'MyModule.Customer';
-- Search the strings table
SELECT * FROM CATALOG.STRINGS
WHERE strings MATCH 'error'
LIMIT 10;
Available Tables
| Table | Contents | Populated By |
|---|---|---|
MODULES | Module names and IDs | REFRESH CATALOG |
ENTITIES | Entity names, persistence, attribute counts | REFRESH CATALOG |
MICROFLOWS | Microflow names, activity counts, parameters | REFRESH CATALOG |
NANOFLOWS | Nanoflow names and metadata | REFRESH CATALOG |
PAGES | Page names, layouts, URLs | REFRESH CATALOG |
SNIPPETS | Snippet names | REFRESH CATALOG |
ENUMERATIONS | Enumeration names and value counts | REFRESH CATALOG |
WORKFLOWS | Workflow names and activity counts | REFRESH CATALOG |
ACTIVITIES | Individual microflow activities | REFRESH CATALOG FULL |
WIDGETS | Widget instances across all pages | REFRESH CATALOG FULL |
REFS | Cross-references between documents | REFRESH CATALOG FULL |
PERMISSIONS | Security permissions (entity access, microflow access) | REFRESH CATALOG FULL |
STRINGS | FTS5 full-text search index | REFRESH CATALOG FULL |
SOURCE | MDL source text for all documents | REFRESH CATALOG FULL |
Implementation
The catalog is implemented in mdl/catalog/catalog.go. It:
- Creates an in-memory SQLite database
- Iterates all modules and documents in the project
- Inserts rows into the appropriate tables
- Builds FTS5 indexes for full-text search
- Builds the reference graph for cross-reference queries
The catalog lives for the duration of the mxcli session and is discarded on exit.
Related Pages
- SQLite Schema – table definitions and indexes
- FTS5 Full-Text Search – how full-text search works
- Reference Tracking – callers, callees, and impact analysis
SQLite Schema
The catalog uses an in-memory SQLite database with the following table definitions.
Core Tables
MODULES
CREATE TABLE MODULES (
Name TEXT PRIMARY KEY,
ModuleID TEXT,
SortIndex INTEGER
);
ENTITIES
CREATE TABLE ENTITIES (
Name TEXT PRIMARY KEY, -- Qualified: Module.Entity
ModuleName TEXT,
EntityName TEXT,
Persistent BOOLEAN,
AttributeCount INTEGER,
Documentation TEXT
);
MICROFLOWS
CREATE TABLE MICROFLOWS (
Name TEXT PRIMARY KEY, -- Qualified: Module.Microflow
ModuleName TEXT,
MicroflowName TEXT,
ActivityCount INTEGER,
ParameterCount INTEGER,
ReturnType TEXT,
Folder TEXT,
Documentation TEXT
);
NANOFLOWS
CREATE TABLE NANOFLOWS (
Name TEXT PRIMARY KEY,
ModuleName TEXT,
NanoflowName TEXT,
ActivityCount INTEGER,
Documentation TEXT
);
PAGES
CREATE TABLE PAGES (
Name TEXT PRIMARY KEY,
ModuleName TEXT,
PageName TEXT,
Layout TEXT,
Url TEXT,
Documentation TEXT
);
SNIPPETS
CREATE TABLE SNIPPETS (
Name TEXT PRIMARY KEY,
ModuleName TEXT,
SnippetName TEXT
);
ENUMERATIONS
CREATE TABLE ENUMERATIONS (
Name TEXT PRIMARY KEY,
ModuleName TEXT,
EnumName TEXT,
ValueCount INTEGER
);
WORKFLOWS
CREATE TABLE WORKFLOWS (
Name TEXT PRIMARY KEY,
ModuleName TEXT,
WorkflowName TEXT,
ActivityCount INTEGER
);
Full Refresh Tables
These tables are only populated by REFRESH CATALOG FULL.
ACTIVITIES
CREATE TABLE ACTIVITIES (
DocumentName TEXT, -- Parent microflow/nanoflow
ActivityType TEXT, -- e.g., "CreateObjectAction", "CallMicroflowAction"
Caption TEXT, -- Activity caption/description
SortOrder INTEGER -- Order within the flow
);
WIDGETS
CREATE TABLE WIDGETS (
DocumentName TEXT, -- Parent page/snippet
WidgetName TEXT, -- Widget instance name
WidgetType TEXT, -- e.g., "Forms$TextBox", "CustomWidgets$ComboBox"
ModuleName TEXT
);
REFS
CREATE TABLE REFS (
SourceName TEXT, -- Referencing document
SourceKind TEXT, -- "Microflow", "Page", etc.
TargetName TEXT, -- Referenced element
TargetKind TEXT, -- "Entity", "Microflow", etc.
RefKind TEXT -- "Call", "DataSource", "Association", etc.
);
CREATE INDEX idx_refs_source ON REFS(SourceName);
CREATE INDEX idx_refs_target ON REFS(TargetName);
PERMISSIONS
CREATE TABLE PERMISSIONS (
RoleName TEXT, -- Module role
TargetName TEXT, -- Entity, microflow, or page
TargetKind TEXT, -- "Entity", "Microflow", "Page"
Permission TEXT -- "Create", "Read", "Write", "Delete", "Execute", "View"
);
Full-Text Search Tables
STRINGS (FTS5)
CREATE VIRTUAL TABLE STRINGS USING fts5(
name, -- Document qualified name
kind, -- Document type
strings, -- All text content concatenated
tokenize='porter unicode61'
);
SOURCE (FTS5)
CREATE VIRTUAL TABLE SOURCE USING fts5(
name, -- Document qualified name
kind, -- Document type
source, -- MDL source representation
tokenize='porter unicode61'
);
Querying Examples
-- Find entities with many attributes
SELECT Name, AttributeCount FROM CATALOG.ENTITIES
WHERE AttributeCount > 20 ORDER BY AttributeCount DESC;
-- Find all references to an entity
SELECT SourceName, RefKind FROM CATALOG.REFS
WHERE TargetName = 'Sales.Customer';
-- Full-text search
SELECT name, kind, snippet(STRINGS, 2, '<b>', '</b>', '...', 20)
FROM CATALOG.STRINGS WHERE strings MATCH 'validation error';
FTS5 Full-Text Search
The catalog uses SQLite’s FTS5 extension to provide fast full-text search across all project content.
Overview
Two FTS5 virtual tables are created during REFRESH CATALOG FULL:
| Table | Indexes | Use Case |
|---|---|---|
STRINGS | Validation messages, captions, labels, log messages | SEARCH 'keyword' |
SOURCE | MDL source representation of all documents | Code search |
Both use the porter unicode61 tokenizer, which provides:
- Porter stemming – matches word variants (e.g., “validate” matches “validation”)
- Unicode support – handles non-ASCII characters correctly
Building the Index
During REFRESH CATALOG FULL, the catalog:
- Iterates all documents in the project
- Extracts all text content (messages, captions, names, expressions)
- Generates the MDL source representation for each document
- Inserts into the FTS5 tables
Querying
From MDL
-- Simple keyword search
SEARCH 'validation';
-- Phrase search
SEARCH 'error message';
From SQL
-- Basic match
SELECT name, kind FROM CATALOG.STRINGS
WHERE strings MATCH 'customer';
-- Phrase match
SELECT name, kind FROM CATALOG.STRINGS
WHERE strings MATCH '"email validation"';
-- Boolean operators
SELECT name, kind FROM CATALOG.STRINGS
WHERE strings MATCH 'customer AND NOT deleted';
-- With snippets (highlighted context)
SELECT name, kind,
snippet(STRINGS, 2, '>>>', '<<<', '...', 20) as context
FROM CATALOG.STRINGS
WHERE strings MATCH 'error'
LIMIT 10;
-- Search MDL source
SELECT name, kind FROM CATALOG.SOURCE
WHERE source MATCH 'RETRIEVE FROM';
FTS5 Query Syntax
FTS5 supports a rich query syntax:
| Syntax | Meaning |
|---|---|
word | Match documents containing “word” |
"exact phrase" | Match exact phrase |
word1 AND word2 | Both words must appear |
word1 OR word2 | Either word may appear |
NOT word | Exclude documents with this word |
word* | Prefix match (e.g., “valid*” matches “validation”) |
NEAR(word1 word2, 5) | Words within 5 tokens of each other |
CLI Integration
The SEARCH command and mxcli search subcommand are wrappers around FTS5 queries:
# Search from command line
mxcli search -p app.mpr "validation"
# Pipe-friendly output
mxcli search -p app.mpr "error" -q --format names
# JSON output
mxcli search -p app.mpr "Customer" -q --format json
The search results include:
- Document type (Microflow, Page, Entity, etc.)
- Qualified name
- Matching context snippet
Performance
FTS5 indexes are built in memory and provide sub-millisecond query times for typical project sizes. The index construction itself takes a few seconds for large projects (1000+ documents), which is why it requires the explicit REFRESH CATALOG FULL command.
Reference Tracking
The cross-reference tracking system powers the callers, callees, references, and impact analysis queries. It is built during REFRESH CATALOG FULL and stored in the REFS table.
How References Are Collected
During a full catalog refresh, every document is analyzed for outgoing references:
| Document Type | Reference Sources |
|---|---|
| Microflows | CALL MICROFLOW actions, RETRIEVE data sources, entity parameters, SHOW PAGE actions, association traversals |
| Nanoflows | Same as microflows (client-side) |
| Pages | Data source entities, microflow data sources, SHOW_PAGE actions, association paths, snippet calls |
| Snippets | Same as pages |
| Domain Models | Generalization references, association endpoints |
Each reference is stored as a row in the REFS table:
INSERT INTO REFS (SourceName, SourceKind, TargetName, TargetKind, RefKind)
VALUES ('Sales.ProcessOrder', 'Microflow', 'Sales.Customer', 'Entity', 'Retrieve');
Query Commands
SHOW CALLERS
Find what calls a given element:
SHOW CALLERS OF Sales.ProcessOrder;
Returns all microflows, nanoflows, and pages that reference Sales.ProcessOrder.
Transitive callers follow the call chain recursively:
SHOW CALLERS OF Sales.ProcessOrder TRANSITIVE;
SHOW CALLEES
Find what a microflow calls:
SHOW CALLEES OF Sales.ProcessOrder;
Returns all microflows, entities, and pages referenced by Sales.ProcessOrder.
SHOW REFERENCES
Find all references to an element:
SHOW REFERENCES TO Sales.Customer;
Returns every document that references Sales.Customer in any way (data source, parameter, association, etc.).
SHOW IMPACT
Analyze the impact of changing an element:
SHOW IMPACT OF Sales.Customer;
Returns all direct and transitive dependents – everything that would potentially be affected by changing or removing the element.
SHOW CONTEXT
Assemble context for understanding a document:
SHOW CONTEXT OF Sales.ProcessOrder DEPTH 3;
Returns the element itself plus its callers and callees up to the specified depth, providing a focused view of the element’s neighborhood in the dependency graph.
CLI Commands
The same queries are available as CLI subcommands:
# Direct callers
mxcli callers -p app.mpr Sales.ProcessOrder
# Transitive callers
mxcli callers -p app.mpr Sales.ProcessOrder --transitive
# Callees
mxcli callees -p app.mpr Sales.ProcessOrder
# All references
mxcli refs -p app.mpr Sales.Customer
# Impact analysis
mxcli impact -p app.mpr Sales.Customer
# Context assembly
mxcli context -p app.mpr Sales.ProcessOrder --depth 3
Reference Kinds
The RefKind column in the REFS table categorizes the relationship:
| RefKind | Meaning |
|---|---|
Call | Microflow/nanoflow call action |
Retrieve | Database retrieve with entity data source |
DataSource | Page/widget data source reference |
ShowPage | Show page action |
Parameter | Microflow parameter type reference |
Association | Association traversal |
Generalization | Entity generalization (inheritance) |
SnippetCall | Snippet embedded in a page |
Enumeration | Enumeration type reference |
Implementation
The reference tracker is implemented in the catalog package (mdl/catalog/catalog.go). It:
- Parses each document’s BSON structure
- Extracts all qualified name references
- Classifies each reference by kind
- Inserts into the
REFStable with source/target/kind
Transitive queries use recursive SQL CTEs (Common Table Expressions) to follow reference chains.
MDL Quick Reference
Complete syntax reference for MDL (Mendix Definition Language). This is the authoritative reference for all MDL statement syntax.
Entity Generalization (EXTENDS)
CRITICAL: EXTENDS goes BEFORE the opening parenthesis, not after!
-- Correct: EXTENDS before (
CREATE PERSISTENT ENTITY Module.ProductPhoto EXTENDS System.Image (
PhotoCaption: String(200)
);
-- Wrong: EXTENDS after ) = parse error!
CREATE PERSISTENT ENTITY Module.Photo (
PhotoCaption: String(200)
) EXTENDS System.Image;
Domain Model
| Statement | Syntax | Notes |
|---|---|---|
| Create entity | CREATE [OR MODIFY] PERSISTENT|NON-PERSISTENT ENTITY Module.Name (attrs); | Persistent is default |
| Create with extends | CREATE PERSISTENT ENTITY Module.Name EXTENDS Parent.Entity (attrs); | EXTENDS before ( |
| Create view entity | CREATE VIEW ENTITY Module.Name (attrs) AS SELECT ...; | OQL-backed read-only |
| Create external entity | CREATE EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs); | From consumed OData |
| Drop entity | DROP ENTITY Module.Name; | |
| Describe entity | DESCRIBE ENTITY Module.Name; | Full MDL output |
| Show entities | SHOW ENTITIES [IN Module]; | List all or filter by module |
| Create enumeration | CREATE [OR MODIFY] ENUMERATION Module.Name (Value1 'Caption', ...); | |
| Drop enumeration | DROP ENUMERATION Module.Name; | |
| Create association | CREATE ASSOCIATION Module.Name FROM Parent TO Child TYPE Reference|ReferenceSet [OWNER Default|Both] [DELETE_BEHAVIOR ...]; | |
| Drop association | DROP ASSOCIATION Module.Name; |
ALTER ENTITY
Modifies an existing entity without full replacement.
| Operation | Syntax | Notes |
|---|---|---|
| Add attributes | ALTER ENTITY Module.Name ADD (Attr: Type [constraints]); | One or more attributes |
| Drop attributes | ALTER ENTITY Module.Name DROP (AttrName, ...); | |
| Modify attributes | ALTER ENTITY Module.Name MODIFY (Attr: NewType [constraints]); | Change type/constraints |
| Rename attribute | ALTER ENTITY Module.Name RENAME OldName TO NewName; | |
| Add index | ALTER ENTITY Module.Name ADD INDEX (Col1 [ASC|DESC], ...); | |
| Drop index | ALTER ENTITY Module.Name DROP INDEX (Col1, ...); | |
| Set documentation | ALTER ENTITY Module.Name SET DOCUMENTATION 'text'; |
Example:
ALTER ENTITY Sales.Customer
ADD (Phone: String(50), Notes: String(unlimited));
ALTER ENTITY Sales.Customer
RENAME Phone TO PhoneNumber;
ALTER ENTITY Sales.Customer
ADD INDEX (Email);
Constants
| Statement | Syntax | Notes |
|---|---|---|
| Show constants | SHOW CONSTANTS [IN Module]; | List all or filter by module |
| Describe constant | DESCRIBE CONSTANT Module.Name; | Full MDL output |
| Create constant | CREATE [OR MODIFY] CONSTANT Module.Name TYPE DataType DEFAULT 'value'; | String, Integer, Boolean, etc. |
| Drop constant | DROP CONSTANT Module.Name; |
Example:
CREATE CONSTANT MyModule.ApiBaseUrl TYPE String DEFAULT 'https://api.example.com';
CREATE CONSTANT MyModule.MaxRetries TYPE Integer DEFAULT 3;
CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
OData Clients, Services & External Entities
| Statement | Syntax | Notes |
|---|---|---|
| Show OData clients | SHOW ODATA CLIENTS [IN Module]; | Consumed OData services |
| Describe OData client | DESCRIBE ODATA CLIENT Module.Name; | Full MDL output |
| Create OData client | CREATE [OR MODIFY] ODATA CLIENT Module.Name (...); | Version, MetadataUrl, Timeout, etc. |
| Alter OData client | ALTER ODATA CLIENT Module.Name SET Key = Value; | |
| Drop OData client | DROP ODATA CLIENT Module.Name; | |
| Show OData services | SHOW ODATA SERVICES [IN Module]; | Published OData services |
| Describe OData service | DESCRIBE ODATA SERVICE Module.Name; | Full MDL output |
| Create OData service | CREATE [OR MODIFY] ODATA SERVICE Module.Name (...) AUTHENTICATION ... { PUBLISH ENTITY ... }; | |
| Alter OData service | ALTER ODATA SERVICE Module.Name SET Key = Value; | |
| Drop OData service | DROP ODATA SERVICE Module.Name; | |
| Show external entities | SHOW EXTERNAL ENTITIES [IN Module]; | OData-backed entities |
| Create external entity | CREATE [OR MODIFY] EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs); | |
| Grant OData access | GRANT ACCESS ON ODATA SERVICE Module.Name TO Module.Role, ...; | |
| Revoke OData access | REVOKE ACCESS ON ODATA SERVICE Module.Name FROM Module.Role, ...; |
OData Client Example:
CREATE ODATA CLIENT MyModule.ExternalAPI (
Version: '1.0',
ODataVersion: OData4,
MetadataUrl: 'https://api.example.com/odata/v4/$metadata',
Timeout: 300
);
OData Service Example:
CREATE ODATA SERVICE MyModule.CustomerAPI (
Path: '/odata/customers',
Version: '1.0.0',
ODataVersion: OData4,
Namespace: 'MyModule.Customers'
)
AUTHENTICATION Basic, Session
{
PUBLISH ENTITY MyModule.Customer AS 'Customers' (
ReadMode: SOURCE,
InsertMode: SOURCE,
UpdateMode: NOT_SUPPORTED,
DeleteMode: NOT_SUPPORTED,
UsePaging: Yes,
PageSize: 100
)
EXPOSE (Name, Email, Phone);
};
Microflows - Supported Statements
| Statement | Syntax | Notes |
|---|---|---|
| Variable declaration | DECLARE $Var Type = value; | Primitives: String, Integer, Boolean, Decimal, DateTime |
| Entity declaration | DECLARE $Entity Module.Entity; | No AS keyword, no = empty |
| List declaration | DECLARE $List List of Module.Entity = empty; | |
| Assignment | SET $Var = expression; | Variable must be declared first |
| Create object | $Var = CREATE Module.Entity (Attr = value); | |
| Change object | CHANGE $Entity (Attr = value); | |
| Commit | COMMIT $Entity [WITH EVENTS] [REFRESH]; | |
| Delete | DELETE $Entity; | |
| Rollback | ROLLBACK $Entity [REFRESH]; | Reverts uncommitted changes |
| Retrieve (DB) | RETRIEVE $Var FROM Module.Entity [WHERE condition]; | Database XPath retrieve |
| Retrieve (Assoc) | RETRIEVE $List FROM $Parent/Module.AssocName; | Retrieve by association |
| Call microflow | $Result = CALL MICROFLOW Module.Name (Param = $value); | |
| Call nanoflow | $Result = CALL NANOFLOW Module.Name (Param = $value); | |
| Show page | SHOW PAGE Module.PageName ($Param = $value); | Also accepts (Param: $value) |
| Close page | CLOSE PAGE; | |
| Validation | VALIDATION FEEDBACK $Entity/Attribute MESSAGE 'message'; | Requires attribute path + MESSAGE |
| Log | LOG INFO|WARNING|ERROR [NODE 'name'] 'message'; | |
| Position | @position(x, y) | Canvas position (before activity) |
| Caption | @caption 'text' | Custom caption (before activity) |
| Color | @color Green | Background color (before activity) |
| Annotation | @annotation 'text' | Visual note attached to next activity |
| IF | IF condition THEN ... [ELSE ...] END IF; | |
| LOOP | LOOP $Item IN $List BEGIN ... END LOOP; | FOR EACH over list |
| WHILE | WHILE condition BEGIN ... END WHILE; | Condition-based loop |
| Return | RETURN $value; | Required at end of every flow path |
| Execute DB query | $Result = EXECUTE DATABASE QUERY Module.Conn.Query; | 3-part name; supports DYNAMIC, params, CONNECTION override |
| Error handling | ... ON ERROR CONTINUE|ROLLBACK|{ handler }; | Not supported on EXECUTE DATABASE QUERY |
Microflows - NOT Supported (Will Cause Parse Errors)
| Unsupported | Use Instead | Notes |
|---|---|---|
CASE ... WHEN ... END CASE | Nested IF ... ELSE ... END IF | Switch not implemented |
TRY ... CATCH ... END TRY | ON ERROR { ... } blocks | Use error handlers on specific activities |
Notes:
RETRIEVE ... LIMIT nIS supported.LIMIT 1returns a single entity, otherwise returns a list.ROLLBACK $Entity [REFRESH];IS supported. Rolls back uncommitted changes to an object.
Project Organization
| Statement | Syntax | Notes |
|---|---|---|
| Microflow folder | FOLDER 'path' (before BEGIN) | CREATE MICROFLOW ... FOLDER 'ACT' BEGIN ... END; |
| Page folder | Folder: 'path' (in properties) | CREATE PAGE ... (Folder: 'Pages/Detail') { ... } |
| Move to folder | MOVE PAGE|MICROFLOW|SNIPPET|NANOFLOW|ENUMERATION Module.Name TO FOLDER 'path'; | Folders created automatically |
| Move to module root | MOVE PAGE Module.Name TO Module; | Removes from folder |
| Move across modules | MOVE PAGE Old.Name TO NewModule; | Breaks by-name references – use SHOW IMPACT OF first |
| Move to folder in other module | MOVE PAGE Old.Name TO FOLDER 'path' IN NewModule; | |
| Move entity to module | MOVE ENTITY Old.Name TO NewModule; | Entities don’t support folders |
Nested folders use / separator: 'Parent/Child/Grandchild'. Missing folders are auto-created.
Security Management
| Statement | Syntax | Notes |
|---|---|---|
| Show project security | SHOW PROJECT SECURITY; | Displays security level, admin, demo users |
| Show module roles | SHOW MODULE ROLES [IN Module]; | All roles or filtered by module |
| Show user roles | SHOW USER ROLES; | Project-level user roles |
| Show demo users | SHOW DEMO USERS; | Configured demo users |
| Show access on element | SHOW ACCESS ON MICROFLOW|PAGE|Entity Mod.Name; | Which roles can access |
| Show security matrix | SHOW SECURITY MATRIX [IN Module]; | Full access overview |
| Create module role | CREATE MODULE ROLE Mod.Role [DESCRIPTION 'text']; | |
| Drop module role | DROP MODULE ROLE Mod.Role; | |
| Create user role | CREATE USER ROLE Name (Mod.Role, ...) [MANAGE ALL ROLES]; | Aggregates module roles |
| Alter user role | ALTER USER ROLE Name ADD|REMOVE MODULE ROLES (Mod.Role, ...); | |
| Drop user role | DROP USER ROLE Name; | |
| Grant microflow access | GRANT EXECUTE ON MICROFLOW Mod.MF TO Mod.Role, ...; | |
| Revoke microflow access | REVOKE EXECUTE ON MICROFLOW Mod.MF FROM Mod.Role, ...; | |
| Grant page access | GRANT VIEW ON PAGE Mod.Page TO Mod.Role, ...; | |
| Revoke page access | REVOKE VIEW ON PAGE Mod.Page FROM Mod.Role, ...; | |
| Grant entity access | GRANT Mod.Role ON Mod.Entity (CREATE, DELETE, READ *, WRITE *); | Supports member lists and WHERE |
| Revoke entity access | REVOKE Mod.Role ON Mod.Entity; | |
| Set security level | ALTER PROJECT SECURITY LEVEL OFF|PROTOTYPE|PRODUCTION; | |
| Toggle demo users | ALTER PROJECT SECURITY DEMO USERS ON|OFF; | |
| Create demo user | CREATE DEMO USER 'name' PASSWORD 'pass' [ENTITY Module.Entity] (UserRole, ...); | |
| Drop demo user | DROP DEMO USER 'name'; |
Workflows
| Statement | Syntax | Notes |
|---|---|---|
| Show workflows | SHOW WORKFLOWS [IN Module]; | List all or filter by module |
| Describe workflow | DESCRIBE WORKFLOW Module.Name; | Full MDL output |
| Create workflow | CREATE [OR MODIFY] WORKFLOW Module.Name PARAMETER $Ctx: Module.Entity BEGIN ... END WORKFLOW; | See activity types below |
| Drop workflow | DROP WORKFLOW Module.Name; | |
| Grant workflow access | GRANT EXECUTE ON WORKFLOW Module.Name TO Mod.Role, ...; | |
| Revoke workflow access | REVOKE EXECUTE ON WORKFLOW Module.Name FROM Mod.Role, ...; |
Workflow Activity Types:
USER TASK <name> '<caption>' [PAGE Mod.Page] [TARGETING MICROFLOW Mod.MF] [OUTCOMES '<out>' { } ...];CALL MICROFLOW Mod.MF [COMMENT '<text>'] [OUTCOMES '<out>' { } ...];CALL WORKFLOW Mod.WF [COMMENT '<text>'];DECISION ['<caption>'] OUTCOMES '<out>' { } ...;PARALLEL SPLIT PATH 1 { } PATH 2 { };JUMP TO <activity-name>;WAIT FOR TIMER ['<expr>'];WAIT FOR NOTIFICATION;END;
Example:
CREATE WORKFLOW Module.ApprovalFlow
PARAMETER $Context: Module.Request
OVERVIEW PAGE Module.WorkflowOverview
BEGIN
USER TASK ReviewTask 'Review the request'
PAGE Module.ReviewPage
OUTCOMES 'Approve' { } 'Reject' { };
END WORKFLOW;
Project Structure
| Statement | Syntax | Notes |
|---|---|---|
| Structure overview | SHOW STRUCTURE; | Depth 2 (elements with signatures), user modules only |
| Module counts | SHOW STRUCTURE DEPTH 1; | One line per module with element counts |
| Full types | SHOW STRUCTURE DEPTH 3; | Typed attributes, named parameters |
| Filter by module | SHOW STRUCTURE IN ModuleName; | Single module only |
| Include all modules | SHOW STRUCTURE DEPTH 1 ALL; | Include system/marketplace modules |
Navigation
| Statement | Syntax | Notes |
|---|---|---|
| Show navigation | SHOW NAVIGATION; | Summary of all profiles |
| Show menu tree | SHOW NAVIGATION MENU [Profile]; | Menu tree for profile or all |
| Show home pages | SHOW NAVIGATION HOMES; | Home page assignments across profiles |
| Describe navigation | DESCRIBE NAVIGATION [Profile]; | Full MDL output (round-trippable) |
| Create/replace navigation | CREATE OR REPLACE NAVIGATION Profile ...; | Full replacement of profile |
Navigation Example:
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
HOME PAGE MyModule.AdminHome FOR MyModule.Administrator
LOGIN PAGE Administration.Login
NOT FOUND PAGE MyModule.Custom404
MENU (
MENU ITEM 'Home' PAGE MyModule.Home_Web;
MENU 'Admin' (
MENU ITEM 'Users' PAGE Administration.Account_Overview;
);
);
Project Settings
| Statement | Syntax | Notes |
|---|---|---|
| Show settings | SHOW SETTINGS; | Overview of all settings parts |
| Describe settings | DESCRIBE SETTINGS; | Full MDL output (round-trippable) |
| Alter model settings | ALTER SETTINGS MODEL Key = Value; | AfterStartupMicroflow, HashAlgorithm, JavaVersion, etc. |
| Alter configuration | ALTER SETTINGS CONFIGURATION 'Name' Key = Value; | DatabaseType, DatabaseUrl, HttpPortNumber, etc. |
| Alter constant | ALTER SETTINGS CONSTANT 'Name' VALUE 'val' IN CONFIGURATION 'cfg'; | Override constant per configuration |
| Alter language | ALTER SETTINGS LANGUAGE Key = Value; | DefaultLanguageCode |
| Alter workflows | ALTER SETTINGS WORKFLOWS Key = Value; | UserEntity, DefaultTaskParallelism |
Business Events
| Statement | Syntax | Notes |
|---|---|---|
| Show services | SHOW BUSINESS EVENTS; | List all business event services |
| Show in module | SHOW BUSINESS EVENTS IN Module; | Filter by module |
| Describe service | DESCRIBE BUSINESS EVENT SERVICE Module.Name; | Full MDL output |
| Create service | CREATE BUSINESS EVENT SERVICE Module.Name (...) { MESSAGE ... }; | See help topic for full syntax |
| Drop service | DROP BUSINESS EVENT SERVICE Module.Name; | Delete a service |
Java Actions
| Statement | Syntax | Notes |
|---|---|---|
| Show Java actions | SHOW JAVA ACTIONS [IN Module]; | List all or filtered by module |
| Describe Java action | DESCRIBE JAVA ACTION Module.Name; | Full MDL output with signature |
| Create Java action | CREATE JAVA ACTION Module.Name(params) RETURNS type AS $$ ... $$; | Inline Java code |
| Create with type params | CREATE JAVA ACTION Module.Name(EntityType: ENTITY <pEntity>, Obj: pEntity) ...; | Generic type parameters |
| Create exposed action | ... EXPOSED AS 'Caption' IN 'Category' AS $$ ... $$; | Toolbox-visible in Studio Pro |
| Drop Java action | DROP JAVA ACTION Module.Name; | Delete a Java action |
| Call from microflow | $Result = CALL JAVA ACTION Module.Name(Param = value); | Inside BEGIN…END |
Parameter Types: String, Integer, Long, Decimal, Boolean, DateTime, Module.Entity, List of Module.Entity, ENUM Module.EnumName, Enumeration(Module.EnumName), StringTemplate(Sql), StringTemplate(Oql), ENTITY <pEntity> (type parameter declaration), bare pEntity (type parameter reference).
Type Parameters allow generic entity handling. ENTITY <pEntity> declares the type parameter inline and becomes the entity type selector; bare pEntity parameters receive entity instances:
CREATE JAVA ACTION Module.Validate(
EntityType: ENTITY <pEntity> NOT NULL,
InputObject: pEntity NOT NULL
) RETURNS Boolean
EXPOSED AS 'Validate Entity' IN 'Validation'
AS $$
return InputObject != null;
$$;
Pages
MDL uses explicit property declarations for pages:
| Element | Syntax | Example |
|---|---|---|
| Page properties | (Key: value, ...) | (Title: 'Edit', Layout: Atlas_Core.Atlas_Default) |
| Page variables | Variables: { $name: Type = 'expr' } | Variables: { $show: Boolean = 'true' } |
| Widget name | Required after type | TEXTBOX txtName (...) |
| Attribute binding | Attribute: AttrName | TEXTBOX txt (Label: 'Name', Attribute: Name) |
| Variable binding | DataSource: $Var | DATAVIEW dv (DataSource: $Product) { ... } |
| Action binding | Action: TYPE | ACTIONBUTTON btn (Caption: 'Save', Action: SAVE_CHANGES) |
| Microflow action | Action: MICROFLOW Name(Param: val) | Action: MICROFLOW Mod.ACT_Process(Order: $Order) |
| Database source | DataSource: DATABASE Entity | DATAGRID dg (DataSource: DATABASE Module.Entity) |
| Selection binding | DataSource: SELECTION widget | DATAVIEW dv (DataSource: SELECTION galleryList) |
| CSS class | Class: 'classes' | CONTAINER c (Class: 'card mx-spacing-top-large') |
| Inline style | Style: 'css' | CONTAINER c (Style: 'padding: 16px;') |
| Design properties | DesignProperties: [...] | CONTAINER c (DesignProperties: ['Spacing top': 'Large', 'Full width': ON]) |
| Width (pixels) | Width: integer | IMAGE img (Width: 200) |
| Height (pixels) | Height: integer | IMAGE img (Height: 150) |
| Page size | PageSize: integer | DATAGRID dg (PageSize: 25) |
| Pagination mode | Pagination: mode | DATAGRID dg (Pagination: virtualScrolling) |
| Paging position | PagingPosition: pos | DATAGRID dg (PagingPosition: both) |
| Paging buttons | ShowPagingButtons: mode | DATAGRID dg (ShowPagingButtons: auto) |
DataGrid Column Properties:
| Property | Values | Default | Example |
|---|---|---|---|
Attribute | attribute name | (required) | Attribute: Price |
Caption | string | attribute name | Caption: 'Unit Price' |
Alignment | left, center, right | left | Alignment: right |
WrapText | true, false | false | WrapText: true |
Sortable | true, false | true/false | Sortable: false |
Resizable | true, false | true | Resizable: false |
Draggable | true, false | true | Draggable: false |
Hidable | yes, hidden, no | yes | Hidable: no |
ColumnWidth | autoFill, autoFit, manual | autoFill | ColumnWidth: manual |
Size | integer (px) | 1 | Size: 200 |
Visible | expression string | true | Visible: '$showColumn' (page variable, not $currentObject) |
DynamicCellClass | expression string | (empty) | DynamicCellClass: 'if(...) then ... else ...' |
Tooltip | text string | (empty) | Tooltip: 'Price in USD' |
Page Example:
CREATE PAGE MyModule.Customer_Edit
(
Params: { $Customer: MyModule.Customer },
Title: 'Edit Customer',
Layout: Atlas_Core.PopupLayout
)
{
DATAVIEW dvCustomer (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
FOOTER footer1 {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
Supported Widgets:
- Layout:
LAYOUTGRID,ROW,COLUMN,CONTAINER,CUSTOMCONTAINER - Input:
TEXTBOX,TEXTAREA,CHECKBOX,RADIOBUTTONS,DATEPICKER,COMBOBOX - Display:
DYNAMICTEXT,DATAGRID,GALLERY,LISTVIEW,IMAGE,STATICIMAGE,DYNAMICIMAGE - Actions:
ACTIONBUTTON,LINKBUTTON,NAVIGATIONLIST - Structure:
DATAVIEW,HEADER,FOOTER,CONTROLBAR,SNIPPETCALL
ALTER PAGE / ALTER SNIPPET
Modify an existing page or snippet’s widget tree in-place without full CREATE OR REPLACE. Works directly on the raw BSON tree, preserving unsupported widget types.
| Operation | Syntax | Notes |
|---|---|---|
| Set property | SET Caption = 'New' ON widgetName | Single property on a widget |
| Set multiple | SET (Caption = 'Save', ButtonStyle = Success) ON btn | Multiple properties at once |
| Page-level set | SET Title = 'New Title' | No ON clause for page properties |
| Insert after | INSERT AFTER widgetName { widgets } | Add widgets after target |
| Insert before | INSERT BEFORE widgetName { widgets } | Add widgets before target |
| Drop widgets | DROP WIDGET name1, name2 | Remove widgets by name |
| Replace widget | REPLACE widgetName WITH { widgets } | Replace widget subtree |
| Pluggable prop | SET 'showLabel' = false ON cbStatus | Quoted name for pluggable widgets |
| Add variable | ADD Variables $name: Type = 'expr' | Add a page variable |
| Drop variable | DROP Variables $name | Remove a page variable |
Supported SET properties: Caption, Label, ButtonStyle, Class, Style, Editable, Visible, Name, Title (page-level), and quoted pluggable widget properties.
Example:
ALTER PAGE Module.EditPage {
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;
DROP WIDGET txtUnused;
INSERT AFTER txtEmail {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
}
};
ALTER SNIPPET Module.NavMenu {
SET Caption = 'Dashboard' ON btnHome
};
Tip: Run DESCRIBE PAGE Module.PageName first to see widget names.
External SQL Statements
Direct SQL query execution against external databases (PostgreSQL, Oracle, SQL Server). Credentials are isolated – DSN never appears in session output or logs.
| Statement | Syntax | Notes |
|---|---|---|
| Connect | SQL CONNECT <driver> '<dsn>' AS <alias>; | Drivers: postgres, oracle, sqlserver |
| Disconnect | SQL DISCONNECT <alias>; | Closes connection |
| List connections | SQL CONNECTIONS; | Shows alias + driver only (no DSN) |
| Show tables | SQL <alias> SHOW TABLES; | Lists user tables |
| Show views | SQL <alias> SHOW VIEWS; | Lists user views |
| Show functions | SQL <alias> SHOW FUNCTIONS; | Lists functions and procedures |
| Describe table | SQL <alias> DESCRIBE <table>; | Shows columns, types, nullability |
| Query | SQL <alias> <any-sql>; | Raw SQL passthrough |
| Import | IMPORT FROM <alias> QUERY '<sql>' INTO Module.Entity MAP (...) [LINK (...)] [BATCH n] [LIMIT n]; | Insert external data into Mendix app DB |
| Generate connector | SQL <alias> GENERATE CONNECTOR INTO <module> [TABLES (...)] [VIEWS (...)] [EXEC]; | Generate Database Connector MDL from schema |
-- Connect to PostgreSQL
SQL CONNECT postgres 'postgres://user:pass@localhost:5432/mydb' AS source;
-- Explore schema
SQL source SHOW TABLES;
SQL source DESCRIBE users;
-- Query data
SQL source SELECT * FROM users WHERE active = true LIMIT 10;
-- Import external data into Mendix app database
IMPORT FROM source QUERY 'SELECT name, email FROM employees'
INTO HRModule.Employee
MAP (name AS Name, email AS Email);
-- Import with association linking
IMPORT FROM source QUERY 'SELECT name, dept_name FROM employees'
INTO HR.Employee
MAP (name AS Name)
LINK (dept_name TO Employee_Department ON Name);
-- Generate Database Connector from schema
SQL source GENERATE CONNECTOR INTO HRModule;
SQL source GENERATE CONNECTOR INTO HRModule TABLES (employees, departments) EXEC;
-- Manage connections
SQL CONNECTIONS;
SQL DISCONNECT source;
CLI subcommand: mxcli sql --driver postgres --dsn '...' "SELECT 1" (see mxcli syntax sql). Supported drivers: postgres (pg, postgresql), oracle (ora), sqlserver (mssql).
Catalog & Search
| Statement | Syntax | Notes |
|---|---|---|
| Refresh catalog | REFRESH CATALOG; | Rebuild basic metadata tables |
| Refresh with refs | REFRESH CATALOG FULL; | Include cross-references and source |
| Show catalog tables | SHOW CATALOG TABLES; | List available queryable tables |
| Query catalog | SELECT ... FROM CATALOG.<table> [WHERE ...]; | SQL against project metadata |
| Show callers | SHOW CALLERS OF Module.Name; | What calls this element |
| Show callees | SHOW CALLEES OF Module.Name; | What this element calls |
| Show references | SHOW REFERENCES OF Module.Name; | All references to/from |
| Show impact | SHOW IMPACT OF Module.Name; | Impact analysis |
| Show context | SHOW CONTEXT OF Module.Name; | Surrounding context |
| Full-text search | SEARCH '<keyword>'; | Search across all strings and source |
Cross-reference commands require REFRESH CATALOG FULL to populate reference data.
Connection & Session
| Statement | Syntax | Notes |
|---|---|---|
| Connect | CONNECT LOCAL '/path/to/app.mpr'; | Open a Mendix project |
| Disconnect | DISCONNECT; | Close current project |
| Status | STATUS; | Show connection info |
| Refresh | REFRESH; | Reload project from disk |
| Commit | COMMIT [MESSAGE 'text']; | Save changes to MPR |
| Set variable | SET key = value; | Session variable (e.g., output_format = 'json') |
| Exit | EXIT; | Close REPL session |
CLI Commands
| Command | Syntax | Notes |
|---|---|---|
| Interactive REPL | mxcli | Interactive MDL shell |
| Execute command | mxcli -p app.mpr -c "SHOW ENTITIES" | Single command |
| Execute script | mxcli exec script.mdl -p app.mpr | Script file |
| Check syntax | mxcli check script.mdl | Parse-only validation |
| Check references | mxcli check script.mdl -p app.mpr --references | With reference validation |
| Lint project | mxcli lint -p app.mpr [--format json|sarif] | 14 built-in + 27 Starlark rules |
| Report | mxcli report -p app.mpr [--format markdown|json|html] | Best practices report |
| Test | mxcli test tests/ -p app.mpr | .test.mdl / .test.md files |
| Diff script | mxcli diff -p app.mpr changes.mdl | Compare script vs project |
| Diff local | mxcli diff-local -p app.mpr --ref HEAD | Git diff for MPR v2 |
| OQL | mxcli oql -p app.mpr "SELECT ..." | Query running Mendix runtime |
| External SQL | mxcli sql --driver postgres --dsn '...' "SELECT 1" | Direct database query |
| Docker build | mxcli docker build -p app.mpr | Build with PAD patching |
| Docker check | mxcli docker check -p app.mpr | Validate with mx check |
| Diagnostics | mxcli diag [--bundle] | Session logs, version info |
| Init project | mxcli init -p app.mpr | Create .claude/ folder with skills |
| LSP server | mxcli lsp --stdio | Language server for VS Code |
Data Type Mapping Table
Comprehensive mapping between MDL data types, Mendix internal types, and backend representations.
Primitive Types
| MDL Type | Description | Range/Limits |
|---|---|---|
String | Variable-length text (default length 200) | 1 to unlimited characters |
String(n) | Variable-length text with explicit length | 1 to unlimited characters |
Integer | 32-bit signed integer | -2,147,483,648 to 2,147,483,647 |
Long | 64-bit signed integer | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
Decimal | High-precision decimal number | Up to 20 digits total |
Boolean | True/false value | true or false (must have DEFAULT) |
DateTime | Date and time with timezone awareness | UTC timestamp |
Date | Date only (no time component) | Internally stored as DateTime |
AutoNumber | Auto-incrementing integer | Typically combined with NOT NULL UNIQUE |
Binary | Binary data (files, images) | Configurable max size |
HashedString | Securely hashed string (passwords) | One-way hash, cannot be retrieved |
Complex Types
| MDL Type | Description | Example |
|---|---|---|
Enumeration(Module.EnumName) | Reference to an enumeration type | Status: Enumeration(Sales.OrderStatus) |
MDL to Backend Type Mapping
| MDL Type | BSON $Type | Go Type (SDK) |
|---|---|---|
String | DomainModels$StringAttributeType | *StringAttributeType |
String(n) | DomainModels$StringAttributeType + Length | *StringAttributeType{Length: n} |
Integer | DomainModels$IntegerAttributeType | *IntegerAttributeType |
Long | DomainModels$LongAttributeType | *LongAttributeType |
Decimal | DomainModels$DecimalAttributeType | *DecimalAttributeType |
Boolean | DomainModels$BooleanAttributeType | *BooleanAttributeType |
DateTime | DomainModels$DateTimeAttributeType | *DateTimeAttributeType |
Date | DomainModels$DateTimeAttributeType | *DateTimeAttributeType |
AutoNumber | DomainModels$AutoNumberAttributeType | *AutoNumberAttributeType |
Binary | DomainModels$BinaryAttributeType | *BinaryAttributeType |
Enumeration | DomainModels$EnumerationAttributeType | *EnumerationAttributeType |
HashedString | DomainModels$HashedStringAttributeType | *HashedStringAttributeType |
Default Value Mapping
| MDL Default | BSON Structure | Go Structure |
|---|---|---|
DEFAULT 'text' | Value: {$Type: "DomainModels$StoredValue", DefaultValue: "text"} | Value: &AttributeValue{DefaultValue: "text"} |
DEFAULT 123 | Value: {$Type: "DomainModels$StoredValue", DefaultValue: "123"} | Value: &AttributeValue{DefaultValue: "123"} |
DEFAULT TRUE | Value: {$Type: "DomainModels$StoredValue", DefaultValue: "true"} | Value: &AttributeValue{DefaultValue: "true"} |
| (calculated) | Value: {$Type: "DomainModels$CalculatedValue", Microflow: <id>} | Value: &AttributeValue{Type: "CalculatedValue", MicroflowID: id} |
Default Value Syntax by Type
| Type | Default Syntax | Examples |
|---|---|---|
| String | DEFAULT 'value' | DEFAULT '', DEFAULT 'Unknown' |
| Integer | DEFAULT n | DEFAULT 0, DEFAULT -1 |
| Long | DEFAULT n | DEFAULT 0 |
| Decimal | DEFAULT n.n | DEFAULT 0, DEFAULT 0.00, DEFAULT 99.99 |
| Boolean | DEFAULT TRUE/FALSE | DEFAULT TRUE, DEFAULT FALSE |
| AutoNumber | DEFAULT n | DEFAULT 1 (starting value) |
| Enumeration | DEFAULT Module.Enum.Value | DEFAULT Shop.Status.Active, DEFAULT 'Pending' |
Constraints
| Constraint | Syntax | Description |
|---|---|---|
| Not Null | NOT NULL | Value is required |
| Not Null with Error | NOT NULL ERROR 'message' | Required with custom error |
| Unique | UNIQUE | Value must be unique |
| Unique with Error | UNIQUE ERROR 'message' | Unique with custom error |
Constraints must appear in this order:
NOT NULL [ERROR '...']UNIQUE [ERROR '...']DEFAULT <value>
Type Compatibility
MDL does not perform implicit type conversions. Types must match exactly.
Compatible type changes:
String(100)toString(200)– Increasing lengthIntegertoLong– Widening numeric type
Incompatible type changes:
StringtoIntegerBooleantoString- Any type to
AutoNumber(if not empty)
Complete Example
/** Example entity demonstrating all data types */
@Position(100, 100)
CREATE PERSISTENT ENTITY Demo.AllTypes (
/** Auto-generated ID */
Id: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Short text field */
Code: String(10) NOT NULL UNIQUE,
/** Standard text field */
Name: String(200) NOT NULL,
/** Long text field */
Description: String(unlimited),
/** Integer counter */
Counter: Integer DEFAULT 0,
/** Large number */
BigNumber: Long,
/** Money amount */
Amount: Decimal DEFAULT 0.00,
/** Flag field */
IsActive: Boolean DEFAULT TRUE,
/** Timestamp */
CreatedAt: DateTime,
/** Date only */
BirthDate: Date,
/** File attachment */
Attachment: Binary,
/** Status from enumeration */
Status: Enumeration(Demo.Status) DEFAULT 'Active'
)
INDEX (Code)
INDEX (Name, CreatedAt DESC);
Reserved Words
Complete reference for MDL reserved words and quoting rules.
Quoting Rules
Most MDL keywords now work unquoted as entity names, attribute names, parameter names, and module names. Common words like Caption, Check, Content, Format, Index, Label, Range, Select, Source, Status, Text, Title, Type, Value, Item, Version, Production, etc. are all valid without quoting.
Only structural MDL keywords require quoting: Create, Delete, Begin, End, Return, Entity, Module.
Quoted Identifiers
Quoted identifiers escape any reserved word using double-quotes (ANSI SQL style) or backticks (MySQL style):
DESCRIBE ENTITY "ComboBox"."CategoryTreeVE";
SHOW ENTITIES IN "ComboBox";
CREATE PERSISTENT ENTITY Module.VATRate ("Create": DateTime, Rate: Decimal);
Both double-quote and backtick styles are supported. You can mix quoted and unquoted parts: "ComboBox".CategoryTreeVE.
Words That Do NOT Require Quoting
The following common words can be used as identifiers without quoting:
| Category | Words |
|---|---|
| UI terms | Caption, Content, Label, Text, Title, Format, Style |
| Data terms | Index, Range, Select, Source, Status, Type, Value, Item, Version |
| Security terms | Production |
| Other | Check, and most domain-specific terms |
Words That Require Quoting
The following structural MDL keywords must be quoted when used as identifiers:
| Keyword | Example with quoting |
|---|---|
Create | "Create": DateTime |
Delete | "Delete": Boolean |
Begin | "Begin": DateTime |
End | "End": DateTime |
Return | "Return": String(200) |
Entity | "Entity": String(200) |
Module | "Module": String(200) |
Boolean Attribute Defaults
Boolean attributes auto-default to false when no DEFAULT is specified. Mendix Studio Pro requires Boolean attributes to have a default value.
CALCULATED Attributes
CALCULATED marks an attribute as calculated (not stored). Use CALCULATED BY Module.Microflow to specify the calculation microflow. Calculated attributes derive their value from a microflow at runtime.
ButtonStyle Values
ButtonStyle supports all values: Primary, Default, Success, Danger, Warning, Info.
Mendix Version Compatibility
Supported Mendix Studio Pro versions and MPR format mapping.
Supported Versions
mxcli supports Mendix Studio Pro versions 8.x through 11.x.
| Studio Pro Version | MPR Format | Status |
|---|---|---|
| 8.x | v1 | Supported |
| 9.x | v1 | Supported |
| 10.0 – 10.17 | v1 | Supported |
| 10.18+ | v2 | Supported |
| 11.x | v2 | Supported (primary development target) |
Note: Development and testing is primarily done against Mendix 11.6. Other versions are supported but may have untested edge cases.
MPR Format Versions
v1 (Mendix < 10.18)
- Single
.mprSQLite database file - All documents stored as BSON blobs in the
UnitContentstable - Self-contained – one file holds the entire project
v2 (Mendix >= 10.18)
.mprSQLite file for metadata onlymprcontents/folder with individual.mxunitfiles for each document- Better suited for Git version control (smaller, per-document diffs)
The library auto-detects the format. No configuration is needed.
Widget Template Versions
Pluggable widget templates are versioned by Mendix release. The embedded templates cover:
| Mendix Version | Template Set |
|---|---|
| 10.6.0 | DataGrid2, ComboBox, Gallery, and others |
| 11.6.0 | Updated templates for current widgets |
When creating pages with pluggable widgets, the library selects the template matching the project’s Mendix version, falling back to the nearest earlier version if an exact match is not available.
Feature Availability by Version
| Feature | Minimum Version | Notes |
|---|---|---|
| Domain models | 8.x | Full support |
| Microflows | 8.x | 60+ activity types |
| Nanoflows | 8.x | Client-side flows |
| Pages | 8.x | 50+ widget types |
| Pluggable widgets | 9.x | Requires widget templates |
| Workflows | 10.x | User tasks, decisions, parallel splits |
| Business events | 10.x | Event service definitions |
| View entities | 10.x | OQL-backed entities |
| MPR v2 format | 10.18 | Per-document file storage |
| Calculated attributes | 10.x | CALCULATED BY microflow |
MxBuild Compatibility
The mx validation tool must match the project’s Mendix version:
# Auto-download the correct MxBuild version
mxcli setup mxbuild -p app.mpr
# Check the project
~/.mxcli/mxbuild/*/modeler/mx check app.mpr
MxBuild is downloaded on demand and cached in ~/.mxcli/mxbuild/{version}/.
Platform Support
mxcli runs on:
| Platform | Architecture |
|---|---|
| Linux | amd64, arm64 |
| macOS | amd64, arm64 (Apple Silicon) |
| Windows | amd64, arm64 |
No CGO or C compiler is required – the binary is fully statically linked using pure Go dependencies.
Common Mistakes and Anti-Patterns
Frequent errors encountered when writing MDL scripts or using the SDK, with explanations of why they occur and how to avoid them.
EXTENDS Must Appear Before (
CRITICAL: The EXTENDS clause goes BEFORE the opening parenthesis, not after.
-- CORRECT: EXTENDS before (
CREATE PERSISTENT ENTITY Module.ProductPhoto EXTENDS System.Image (
PhotoCaption: String(200)
);
-- WRONG: EXTENDS after ) = parse error!
CREATE PERSISTENT ENTITY Module.Photo (
PhotoCaption: String(200)
) EXTENDS System.Image;
String Attributes Need Explicit Length
Always specify an explicit length for String attributes. While String defaults to 200, it is clearer and safer to use String(200) explicitly. Use String(unlimited) for long text.
-- GOOD: Explicit length
Name: String(200)
Description: String(unlimited)
Code: String(10)
-- AVOID: Implicit default length
Name: String
Never Create Empty List Variables as Loop Sources
If processing imported data, accept the list as a microflow parameter. DECLARE $Items List of ... = empty followed by LOOP $Item IN $Items is always wrong – the loop body will never execute.
-- WRONG: Loop over empty list does nothing
DECLARE $Items List of Module.Item = empty;
LOOP $Item IN $Items BEGIN
-- This never executes!
END LOOP;
-- CORRECT: Accept the list as a parameter
CREATE MICROFLOW Module.ProcessItems
PARAMETER $Items: List of Module.Item
BEGIN
LOOP $Item IN $Items BEGIN
-- Process each item
END LOOP;
RETURN true;
END;
Never Use Nested LOOPs for List Matching
Loop over the primary list and use RETRIEVE with a filter for O(N) lookup. Nested loops are O(N^2).
-- WRONG: O(N^2) nested loops
LOOP $Item IN $Items BEGIN
LOOP $Match IN $Targets BEGIN
IF $Item/Key = $Match/Key THEN
-- ...
END IF;
END LOOP;
END LOOP;
-- CORRECT: O(N) with RETRIEVE
LOOP $Item IN $Items BEGIN
RETRIEVE $Match FROM $Targets WHERE Key = $Item/Key LIMIT 1;
IF $Match != empty THEN
-- ...
END IF;
END LOOP;
Association ParentPointer/ChildPointer Semantics Are Inverted
CRITICAL: Mendix BSON uses inverted naming for association pointers. This is counter-intuitive and a common source of bugs.
| BSON Field | Points To | MDL Keyword |
|---|---|---|
ParentPointer | FROM entity (FK owner) | FROM Module.Child |
ChildPointer | TO entity (referenced) | TO Module.Parent |
For example, CREATE ASSOCIATION Mod.Child_Parent FROM Mod.Child TO Mod.Parent stores:
ParentPointer = Child.$ID(the FROM entity owns the foreign key)ChildPointer = Parent.$ID(the TO entity is being referenced)
This affects entity access rules: MemberAccess entries for associations must only be added to the FROM entity (the one stored in ParentPointer). Adding them to the TO entity triggers CE0066 “Entity access is out of date”.
The same convention applies in domainmodel.Association: ParentID = FROM entity, ChildID = TO entity.
BSON Storage Names vs Qualified Names
Mendix uses different “storage names” in BSON $Type fields than the “qualified names” shown in the TypeScript SDK documentation. Using the wrong name causes TypeCacheUnknownTypeException when opening in Studio Pro.
| Qualified Name (SDK/docs) | Storage Name (BSON $Type) | Note |
|---|---|---|
CreateObjectAction | CreateChangeAction | |
ChangeObjectAction | ChangeAction | |
DeleteObjectAction | DeleteAction | |
CommitObjectsAction | CommitAction | |
RollbackObjectAction | RollbackAction | |
AggregateListAction | AggregateAction | |
ListOperationAction | ListOperationsAction | |
ShowPageAction | ShowFormAction | “Form” was original term for “Page” |
ClosePageAction | CloseFormAction | “Form” was original term for “Page” |
When adding new types, always verify the storage name by:
- Examining existing MPR files with the
mxtool or SQLite browser - Checking the reflection data in the
reference/mendixmodellib/reflection-data/directory - Looking at the parser cases in
sdk/mpr/parser_microflow.go
Mendix Expression String Escaping
When generating Mendix expression strings (e.g., in expressionToString()), single quotes within string literals must be escaped by doubling them: 'it''s here'. Do NOT use backslash escaping (\'). This matches Mendix Studio Pro’s expression syntax.
Microflows: Unsupported Constructs
These constructs will cause parse errors:
| Unsupported | Use Instead |
|---|---|
CASE ... WHEN ... END CASE | Nested IF ... ELSE ... END IF |
TRY ... CATCH ... END TRY | ON ERROR { ... } blocks on specific activities |
Boolean Attributes Must Have Defaults
Mendix Studio Pro requires Boolean attributes to have a DEFAULT value. If no DEFAULT is specified, MDL auto-defaults to false.
-- GOOD: Explicit default
IsActive: Boolean DEFAULT true
-- RISKY: Implicit default to false
Deleted: Boolean
Always Validate Before Presenting to Users
Always run syntax and reference checks before delivering MDL scripts:
# Syntax + anti-pattern check
mxcli check script.mdl
# With reference validation against a project
mxcli check script.mdl -p app.mpr --references
Error Messages Reference
Common error messages from Studio Pro, mxcli, and the MDL parser, with explanations and solutions.
Studio Pro Errors
These errors appear when opening a project in Studio Pro after modification by mxcli.
TypeCacheUnknownTypeException
TypeCacheUnknownTypeException: The type cache does not contain a type
with qualified name DomainModels$Index
Cause: The BSON $Type field uses the qualifiedName instead of the storageName. These are often identical, but not always.
Solution: Check the metamodel reflection data for the correct storage name:
| qualifiedName (wrong) | storageName (correct) |
|---|---|
DomainModels$Entity | DomainModels$EntityImpl |
DomainModels$Index | DomainModels$EntityIndex |
Look up the type in reference/mendixmodellib/reflection-data/<version>-structures.json and use the storageName field value.
CE0463: Widget definition changed
CE0463: The widget definition of 'DataGrid2' has changed.
Cause: The widget’s WidgetObject properties do not match its PropertyTypes schema. This happens when:
- A widget template is missing properties
- Property values have incorrect types
- The template was extracted from a different Mendix version
Solution:
- Create the same widget manually in Studio Pro
- Extract its BSON from the saved project
- Compare the template’s
objectsection against the Studio Pro version - Update
sdk/widgets/templates/<version>/<Widget>.jsonto match
See the debug workflow in .claude/skills/debug-bson.md for step-by-step instructions.
CE0066: Entity access is out of date
CE0066: Entity access for 'MyModule.Customer' is out of date.
Cause: Association MemberAccess entries were added to the wrong entity. In Mendix, association access rules must only be on the FROM entity (the one stored in ParentPointer), not the TO entity.
Solution: Ensure MemberAccess entries for associations are added only to the entity that owns the foreign key (the FROM side of the association). Remove any association MemberAccess entries from the TO entity.
System.ArgumentNullException (ValidationRule)
System.ArgumentNullException: Value cannot be null.
Cause: A validation rule’s Attribute field uses a binary UUID instead of a qualified name string. The metamodel specifies BY_NAME_REFERENCE for this field.
Solution: Use a qualified name string (e.g., "Module.Entity.Attribute") for the Attribute field in ValidationRule BSON, not a binary UUID.
mxcli Parser Errors
Mismatched input
Line 3, Col 42: mismatched input ')' expecting ','
Cause: Syntax error in the MDL statement – typically a missing comma, semicolon, or unmatched bracket.
Solution: Check the MDL syntax at the reported line and column. Common issues:
- Missing commas between attribute definitions
- Missing semicolons at the end of statements
- Unmatched parentheses or curly braces
No viable alternative
Line 1, Col 0: no viable alternative at input 'CREAT'
Cause: Unrecognized keyword or misspelling.
Solution: Check the keyword spelling. MDL keywords are case-insensitive but must be valid. Run mxcli syntax keywords for the full keyword list.
mxcli Execution Errors
Module not found
Error: module 'MyModule' not found in project
Cause: The referenced module does not exist in the .mpr file.
Solution: Check the module name with SHOW MODULES and verify the spelling. Module names are case-sensitive.
Entity not found
Error: entity 'MyModule.Customer' not found
Cause: The referenced entity does not exist in the specified module.
Solution: Check with SHOW ENTITIES IN MyModule. If the entity was just created, ensure the create statement executed successfully before referencing it.
Reference validation failed
Error: unresolved reference 'MyModule.NonExistent' at line 5
Cause: A qualified name references an element that does not exist in the project. This error appears with mxcli check script.mdl -p app.mpr --references.
Solution: Verify the referenced element exists, or create it before the referencing statement.
BSON Serialization Errors
Wrong array prefix
Symptom: Studio Pro fails to load the project or shows garbled data.
Cause: Missing or incorrect integer prefix in BSON arrays. Mendix BSON arrays require a count/type prefix as the first element:
{
"Attributes": [3, { ... }, { ... }]
}
Solution: Ensure all arrays include the correct prefix value (typically 2 or 3). Check existing BSON output for the correct prefix for each array property.
Wrong reference format
Symptom: Studio Pro crashes or shows null reference errors.
Cause: Using BY_ID_REFERENCE (binary UUID) where BY_NAME_REFERENCE (qualified string) is expected, or vice versa.
Solution: Check the metamodel reflection data for the property’s kind field:
BY_ID_REFERENCE-> use binary UUIDBY_NAME_REFERENCE-> use qualified name string (e.g.,"Module.Entity.Attribute")
Glossary
Mendix-specific terms and concepts for developers who are new to the Mendix platform.
A
Association
A relationship between two entities. Analogous to a foreign key in relational databases. Mendix supports Reference (one-to-many or one-to-one) and ReferenceSet (many-to-many) types.
Attribute A property of an entity. Equivalent to a column in a relational database table. Types include String, Integer, Long, Decimal, Boolean, DateTime, AutoNumber, Binary, Enumeration, and HashedString.
Access Rule A security rule that controls which module roles can create, read, write, or delete instances of an entity, and which attributes they can access.
B
BSON Binary JSON. The serialization format Mendix uses to store model elements inside MPR files. Each document (entity, microflow, page, etc.) is stored as a BSON blob.
Building Block A reusable page fragment that can be dragged onto pages in Studio Pro. Similar to a Snippet but intended for the toolbox.
C
Catalog In mxcli, an in-memory SQLite database that indexes project metadata for fast querying, full-text search, and cross-reference navigation.
Constant A named value defined at the module level that can be configured per environment (development, acceptance, production). Used for API keys, URLs, feature flags, etc.
D
DataGrid A widget that displays a list of objects in a tabular format with columns, sorting, and search. The classic DataGrid uses server-side rendering; DataGrid2 is the pluggable widget version.
DataView A widget that displays a single object, providing a context for input widgets (TextBox, DatePicker, etc.) to read and write attributes.
Domain Model The data model for a module, consisting of entities, their attributes, and associations between them. Analogous to an entity-relationship diagram.
E
Entity A data type in the domain model. Equivalent to a table in a relational database. Entities can be persistent (stored in the database), non-persistent (in-memory only), or view (backed by an OQL query).
Enumeration A fixed set of named values (e.g., OrderStatus with values Draft, Active, Closed). Used as attribute types when the set of possible values is known at design time.
Event Handler Logic that runs automatically before or after a commit or delete operation on an entity. Configured on the entity in the domain model.
G
Gallery A pluggable widget that displays a list of objects in a card/tile layout, as opposed to the tabular layout of a DataGrid.
Generalization Entity inheritance. When entity B generalizes entity A, B inherits all of A’s attributes and associations. Analogous to class inheritance in object-oriented programming.
L
Layout A page template that defines the common structure (header, sidebar, footer) shared by multiple pages. Every page references a layout.
LayoutGrid A responsive grid widget based on a 12-column system. Used to create multi-column layouts with breakpoints for phone, tablet, and desktop.
M
MDL (Mendix Definition Language) A SQL-like text language for querying and modifying Mendix projects. The primary interface for mxcli.
Microflow Server-side logic expressed as a visual flow of activities. Microflows can retrieve data, call actions, make decisions, loop over lists, and return values. They run on the Mendix runtime (server).
Module A top-level organizational unit in a Mendix project. Each module contains its own domain model, microflows, pages, enumerations, and security settings. Analogous to a package or namespace.
Module Role A permission role defined within a module. Module roles are assigned to user roles at the project level. Entity access rules, microflow access, and page access are granted to module roles.
MPR
Mendix Project Resource file. The binary file (.mpr) that contains the complete Mendix project model. It is a SQLite database with BSON-encoded documents.
N
Nanoflow Client-side logic that runs in the browser (or native app). Syntactically similar to microflows but with restrictions (no database transactions, limited activity types). Used for offline-capable and low-latency operations.
P
Page A user interface screen in a Mendix application. Pages contain widgets (TextBox, DataGrid, Button, etc.) and reference a layout for their outer structure.
Pluggable Widget A widget built with the Mendix Pluggable Widget API (React-based). Examples: DataGrid2, ComboBox, Gallery. Pluggable widgets use JSON-based property schemas (PropertyTypes) stored in the MPR.
R
Runtime The Mendix server-side execution environment (Java-based) that runs the application. It interprets the model, handles HTTP requests, executes microflows, and manages the database.
S
Scheduled Event A microflow that runs automatically on a timer (e.g., every hour, daily at midnight). Configured with a start time and interval.
Snippet A reusable UI fragment that can be embedded in multiple pages via a SnippetCall widget. Snippets can accept parameters.
Studio Pro
The visual IDE for building Mendix applications. It reads and writes .mpr files. mxcli provides a complementary text-based interface to the same project files.
Storage Name
The internal type identifier used in BSON $Type fields. Often the same as the qualified name, but not always (e.g., DomainModels$EntityImpl is the storage name for DomainModels$Entity).
U
User Role A project-level role that aggregates module roles from one or more modules. End users are assigned user roles, which determine their permissions across the entire application.
W
Widget A UI component on a page. Built-in widgets include TextBox, DataGrid, Button, Container, and LayoutGrid. Pluggable widgets extend this set with third-party components.
Workflow A long-running process with user tasks, decisions, and parallel paths. Workflows model approval processes, multi-step procedures, and human-in-the-loop automation.
TypeScript SDK Equivalence
Mapping between the official Mendix Model SDK (TypeScript) and MDL/modelsdk-go equivalents.
Overview
The Mendix Model SDK is Mendix’s official TypeScript library for programmatic model manipulation. It works through the Mendix Platform API and requires cloud connectivity. MDL and modelsdk-go provide similar capabilities but operate directly on local .mpr files.
Feature Comparison
| Feature | Mendix Model SDK (TypeScript) | MDL / modelsdk-go |
|---|---|---|
| Language | TypeScript / JavaScript | Go (library), MDL (DSL) |
| Runtime | Node.js | Native binary |
| Cloud required | Yes (Platform API) | No (local files) |
| Authentication | API key + PAT | None |
| Real-time collaboration | Yes | No |
| Read operations | Yes | Yes |
| Write operations | Yes | Yes |
| Type safety | TypeScript types | Go types |
| CLI tool | No | Yes (mxcli) |
| SQL-like DSL | No | Yes (MDL) |
| AI assistant integration | Limited | Built-in (multi-tool) |
| Full-text search | No | Yes (FTS5) |
| Cross-reference analysis | No | Yes (callers, impact) |
| Linting | No | Yes (Go + Starlark rules) |
Operation Mapping
Reading
| TypeScript SDK | MDL Equivalent | Go Library |
|---|---|---|
model.allDomainModels() | SHOW ENTITIES | reader.ListDomainModels() |
domainModel.entities | SHOW ENTITIES IN Module | reader.GetDomainModel(id) |
entity.attributes | DESCRIBE ENTITY Module.Name | dm.Entities[i].Attributes |
model.allMicroflows() | SHOW MICROFLOWS | reader.ListMicroflows() |
model.allPages() | SHOW PAGES | reader.ListPages() |
model.allEnumerations() | SHOW ENUMERATIONS | reader.ListEnumerations() |
Writing
| TypeScript SDK | MDL Equivalent | Go Library |
|---|---|---|
domainmodels.Entity.createIn(dm) | CREATE PERSISTENT ENTITY Module.Name (...) | writer.CreateEntity(dmID, entity) |
entity.name = "Foo" | Part of CREATE ENTITY | entity.Name = "Foo" |
domainmodels.Attribute.createIn(entity) | Column in entity definition | writer.AddAttribute(dmID, entityID, attr) |
domainmodels.Association.createIn(dm) | CREATE ASSOCIATION | writer.CreateAssociation(dmID, assoc) |
microflows.Microflow.createIn(folder) | CREATE MICROFLOW | writer.CreateMicroflow(mf) |
pages.Page.createIn(folder) | CREATE PAGE | writer.CreatePage(page) |
enumerations.Enumeration.createIn(module) | CREATE ENUMERATION | writer.CreateEnumeration(enum) |
Security
| TypeScript SDK | MDL Equivalent |
|---|---|
security.ModuleRole.createIn(module) | CREATE MODULE ROLE Module.RoleName |
| Manual access rule construction | GRANT role ON Entity (permissions) |
| Manual user role creation | CREATE USER ROLE Name (ModuleRoles) |
Workflow Comparison
TypeScript SDK Workflow
const client = new MendixPlatformClient();
const app = await client.getApp("app-id");
const workingCopy = await app.createTemporaryWorkingCopy("main");
const model = await workingCopy.openModel();
const dm = model.allDomainModels().filter(d => d.containerAsModule.name === "Sales")[0];
await dm.load();
const entity = domainmodels.Entity.createIn(dm);
entity.name = "Customer";
const attr = domainmodels.Attribute.createIn(entity);
attr.name = "Name";
attr.type = domainmodels.StringAttributeType.create(model);
await model.flushChanges();
await workingCopy.commitToRepository("main");
MDL Equivalent
CREATE PERSISTENT ENTITY Sales.Customer (
Name: String(200)
);
Go Library Equivalent
reader, _ := modelsdk.Open("app.mpr")
modules, _ := reader.ListModules()
dm, _ := reader.GetDomainModel(modules[0].ID)
writer, _ := modelsdk.OpenForWriting("app.mpr")
entity := modelsdk.NewEntity("Customer")
entity.Attributes = append(entity.Attributes,
modelsdk.NewStringAttribute("Name", 200))
writer.CreateEntity(dm.ID, entity)
Key Differences
- No cloud dependency – modelsdk-go works entirely offline with local
.mprfiles - No working copy management – changes are written directly to the file (back up first)
- MDL is declarative – one statement replaces multiple imperative SDK calls
- Token efficiency – MDL uses 5-10x fewer tokens than equivalent JSON model representations, making it better suited for AI assistant context windows
- Built-in search and analysis – full-text search, cross-reference tracking, and impact analysis are not available in the TypeScript SDK
Changelog
Version history and release notes for ModelSDK Go and the mxcli tool.
Releases
See GitHub Releases for the complete version history, including:
- Release notes with feature descriptions
- Pre-built binaries for all platforms (Linux, macOS, Windows – amd64 and arm64)
- Source code archives
Versioning
mxcli follows Semantic Versioning:
- Major – breaking changes to the MDL language or Go library API
- Minor – new features and capabilities (backward-compatible)
- Patch – bug fixes and documentation updates
Status
Alpha-quality software. This project is in early stages. Expect bugs, missing features, and rough edges. Always back up your Mendix project files before using mxcli to modify them.