` |
| 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](#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](#security)
Mendix uses a role-based access control model:
1. **Module roles** are defined per module (e.g., `Sales.Admin`, `Sales.User`)
2. **User roles** are defined at the project level and aggregate module roles
3. **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)
**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)
**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](#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](#whats-next)
- [Part I: Tutorial](#setting-up) – hands-on walkthrough
- [Part II: The MDL Language](#mdl-basics) – complete language guide
- [Glossary](#glossary) – alphabetical reference of all terms
# [Document Conventions](#document-conventions)
This page describes the formatting and notation conventions used throughout this documentation.
## [Code Examples](#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](#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](#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](#terminology)
Mendix-specific terms such as “entity”, “microflow”, “nanoflow”, “module”, and “domain model” follow standard Mendix terminology. See the [Glossary](#glossary) for definitions.
# [5-Minute Quickstart](#5-minute-quickstart)
Get from zero to modifying a Mendix project in 5 minutes. No prior MDL knowledge needed.
## [1. Install mxcli](#1-install-mxcli)
**Option A: Zero install (Playground)**
[](https://codespaces.new/mendixlabs/mxcli-playground)
Open the [mxcli Playground](https://github.com/mendixlabs/mxcli-playground) in a Codespace – mxcli, a sample project, and example scripts are pre-installed. Skip to step 3.
**Option B: Binary download**
Download from the [GitHub Releases page](https://github.com/mendixlabs/mxcli/releases) and extract:
```
# macOS / Linux
tar xzf mxcli_
.tar.gz
sudo mv mxcli /usr/local/bin/
```
**Option C: Build from source**
```
go install github.com/mendixlabs/mxcli/cmd/mxcli@latest
```
Verify: `mxcli --version` should print the version number.
See [Installation](#installation) for all options including Dev Containers.
## [2. Open your project](#2-open-your-project)
```
mxcli -p /path/to/your-app.mpr
```
You’ll see:
```
Connected to: your-app.mpr (Mendix 11.6.3)
MDL REPL - Mendix Definition Language
mdl>
```
> **Don’t have a Mendix project?** Create one with `mxcli new test-app --version 11.8.0` — this downloads MxBuild, creates a blank project, and sets up all tooling automatically.
## [3. Explore what’s there](#3-explore-whats-there)
```
-- List all modules
LIST MODULES;
-- List entities in a module
LIST ENTITIES IN MyFirstModule;
-- See the full structure at a glance
LIST STRUCTURE;
```
## [4. Create something](#4-create-something)
```
-- Create a new entity
CREATE PERSISTENT ENTITY MyFirstModule.Customer (
Name: String(200) NOT NULL,
Email: String(200),
IsActive: Boolean DEFAULT true
);
```
Expected output:
```
Created entity: MyFirstModule.Customer
```
## [5. Verify it works](#5-verify-it-works)
```
-- See what you created
DESCRIBE ENTITY MyFirstModule.Customer;
```
Output:
```
@Position(100, 100)
CREATE OR REPLACE PERSISTENT ENTITY MyFirstModule.Customer (
Name: String(200) NOT NULL,
Email: String(200),
IsActive: Boolean DEFAULT true
);
```
Open the project in Studio Pro – your entity is there.
## [6. Create a microflow](#6-create-a-microflow)
```
CREATE MICROFLOW MyFirstModule.ACT_DeactivateCustomer (
$Customer: MyFirstModule.Customer
)
BEGIN
CHANGE $Customer (IsActive = false);
COMMIT $Customer;
END;
```
## [7. Validate](#7-validate)
```
# Check your project for errors (from a separate terminal)
mxcli check -p /path/to/your-app.mpr
```
No errors? You’re done. Open in Studio Pro and everything is there.
## [What’s next?](#whats-next-1)
| I want to… | Read… |
| --- | --- |
| Explore my project deeper | [SHOW Commands](#show-modules-show-entities) |
| Create pages with widgets | [Creating a Page](#creating-a-page) |
| Use AI to generate code | [Claude Code Integration](#claude-code-integration) |
| Set up for a team | [Skills and CLAUDE.md](#skills-and-claudemd) |
| See everything mxcli can do | [Capabilities Overview](#capabilities-overview) |
| Customize AI generation | [Customizing AI Generation](#customizing-ai-generation) |
## [Common issues](#common-issues)
**“mx command not available”** – Install mxbuild for validation:
```
mxcli setup mxbuild -p your-app.mpr
```
**“CGO not available”** – mxcli uses pure Go SQLite. No C compiler needed. If you see CGO errors, ensure you’re using the official binary or `go install`.
**Project won’t open in Studio Pro after changes** – Close Studio Pro before running mxcli write commands, then reopen. See [F4 sync support](#mendix-version-compatibility) for details.
# [Setting Up](#setting-up)
Before you can start exploring and modifying Mendix projects from the command line, you need three things:
1. **mxcli installed** on your machine
2. **A Mendix project** (an `.mpr` file) to work with
3. **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](#installation-methods)
There are five ways to get started with mxcli:
- **[Playground](#playground-zero-install)** – open the [mxcli Playground](https://github.com/mendixlabs/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.
- **`mxcli new`** (recommended for new projects) – run `mxcli new MyApp --version 11.8.0` to create a new Mendix project from scratch with all tooling and Dev Container configured. One command does everything: downloads MxBuild, creates the project, sets up AI tools, and installs the correct mxcli binary.
- **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 existing projects) – run `mxcli init` on 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 `.mpr` or use version control (Git) before making changes.
# [Installation](#installation)
Pick whichever method suits your situation. If you just want to try mxcli without installing anything, start with the [Playground](#playground-zero-install). If you’re planning to use mxcli on your own project with an AI coding assistant, skip to the [Dev Container](#dev-container-recommended) section.
## [Playground (zero install)](#playground-zero-install)
The fastest way to try mxcli. The [mxcli Playground](https://github.com/mendixlabs/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.
[](https://codespaces.new/mendixlabs/mxcli-playground)
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, GitHub Copilot, OpenCode, 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](#binary-download)
Pre-built binaries are available for Linux, macOS, and Windows on both amd64 and arm64 architectures.
1. Go to the [GitHub Releases page](https://github.com/mendixlabs/mxcli/releases).
2. Download the archive for your platform (e.g., `mxcli_linux_amd64.tar.gz` or `mxcli_darwin_arm64.tar.gz`).
3. 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](#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
```
## [New project from scratch](#new-project-from-scratch)
If you don’t have a Mendix project yet, `mxcli new` creates one with everything configured:
```
mxcli new MyApp --version 11.8.0
```
This single command:
1. Downloads MxBuild for the specified Mendix version
2. Creates a blank Mendix project (`App.mpr`)
3. Sets up AI tooling (`.claude/`, skills, `AGENTS.md`)
4. Configures a Dev Container (`.devcontainer/`)
5. Downloads the correct Linux mxcli binary for the container
Open the resulting `MyApp/` folder in VS Code and click **“Reopen in Container”** — you’re ready to go.
Options:
```
mxcli new MyApp --version 10.24.0 --output-dir ./projects/my-app
mxcli new MyApp --version 11.8.0 --skip-init # Skip AI tooling setup
```
## [Dev Container for existing projects](#dev-container-for-existing-projects)
For an existing Mendix project, `mxcli init` adds AI tooling and a Dev Container configuration.
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](#whats-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](#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](#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](#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 `.mpr` file or make sure the project is under version control (Git). This isn’t just a disclaimer – take it seriously.
## [The `-p` flag](#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](#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](#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](#mpr-format-auto-detection)
Mendix projects come in two formats:
- **v1**: A single `.mpr` SQLite database file (Mendix versions before 10.18)
- **v2**: An `.mpr` metadata file plus an `mprcontents/` 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](#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](#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-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](#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](#running-commands)
Type any MDL statement and press Enter:
```
SHOW MODULES;
```
Results are printed as formatted tables directly in the terminal.
### [Multi-line statements](#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](#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](#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](#exiting)
When you’re done, type either:
```
EXIT;
```
or:
```
QUIT;
```
You can also press `Ctrl+D` to exit.
## [One-off commands with `-c`](#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](#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)](#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](#whats-next-2)
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](#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 ENTITIES` lists all entities; `SHOW MICROFLOWS IN Sales` narrows 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](#what-you-will-learn)
In this chapter, you will:
1. List modules, entities, microflows, pages, and other elements with **SHOW** commands
2. Inspect the full definition of any element with **DESCRIBE**
3. Find elements by keyword with **SEARCH**
4. Get a bird’s-eye view of project structure with **SHOW STRUCTURE**
## [Prerequisites](#prerequisites)
You should have mxcli installed and know how to open a project. If not, work through the [Setting Up](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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).
# [DESCRIBE, 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](#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](#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](#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)
```
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)
```
DESCRIBE ASSOCIATION MyFirstModule.Order_Customer;
```
```
CREATE ASSOCIATION MyFirstModule.Order_Customer
FROM MyFirstModule.Order TO MyFirstModule.Customer
TYPE Reference
OWNER Default
DELETE_BEHAVIOR DeleteRefSetOnly;
```
## [DESCRIBE MODULE](#describe-module)
To see the full contents of a module (all entities, microflows, pages, etc.):
```
DESCRIBE MODULE MyFirstModule;
```
This is especially useful from the CLI:
```
mxcli describe -p app.mpr module MyFirstModule
```
## [Other DESCRIBE targets](#other-describe-targets)
The same pattern works for other element types:
```
DESCRIBE MODULE MyFirstModule;
DESCRIBE WORKFLOW MyFirstModule.ApprovalFlow;
DESCRIBE NANOFLOW MyFirstModule.NAV_GoToDetail;
DESCRIBE SNIPPET MyFirstModule.CustomerCard;
DESCRIBE CONSTANT MyFirstModule.ApiBaseUrl;
DESCRIBE JSON STRUCTURE MyFirstModule.CustomerResponse;
DESCRIBE IMPORT MAPPING MyFirstModule.IMM_Customer;
DESCRIBE EXPORT MAPPING MyFirstModule.EMM_Customer;
DESCRIBE REST CLIENT MyFirstModule.PetStoreAPI;
DESCRIBE IMAGE COLLECTION MyFirstModule.Icons;
DESCRIBE NAVIGATION;
DESCRIBE SETTINGS;
```
## [Full-text search with SEARCH](#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](#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](#combining-show-describe-and-search)
A typical exploration workflow looks like this:
1. **Start broad** with SHOW to see what exists:
```
SHOW MODULES;
SHOW ENTITIES IN Sales;
```
2. **Zoom in** with DESCRIBE on interesting elements:
```
DESCRIBE ENTITY Sales.Order;
DESCRIBE MICROFLOW Sales.ACT_Order_Process;
```
3. **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).
# [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](#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](#depth-levels)
The `DEPTH` option controls how much detail is shown:
### [DEPTH 1 – Module summary](#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)](#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](#default-view) above.
### [DEPTH 3 – Full detail](#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](#filtering-by-module-1)
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](#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](#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](#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](#what-is-next)
Now that you can explore a project, you are ready to start making changes. Continue to [Your First Changes](#your-first-changes) to learn how to create entities, microflows, and pages using MDL.
# [Your First Changes](#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](#before-you-start)
**Always work on a copy.** mxcli writes directly to your `.mpr` file. Before making changes, either:
- Copy the `.mpr` file (and `mprcontents/` 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](#the-workflow)
Every modification follows the same pattern:
1. **Write MDL** – either directly on the command line with `-c`, or in a `.mdl` script file
2. **Check syntax** – run `mxcli check` to catch errors before touching the project
3. **Execute** – apply the changes to the `.mpr` file
4. **Validate** – use `mxcli docker check` for a full Studio Pro-level validation
5. **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](#what-well-build)
Over the next few pages, you’ll create:
| What | MDL statement | Page |
| --- | --- | --- |
| A `Product` entity with attributes | `CREATE PERSISTENT ENTITY` | [Creating an Entity](#creating-an-entity) |
| A microflow that creates products | `CREATE MICROFLOW` | [Creating a Microflow](#creating-a-microflow) |
| An overview page listing products | `CREATE PAGE` | [Creating a Page](#creating-a-page) |
The final page, [Validating with mxcli check](#validating-with-mxcli-check), covers the full validation workflow in detail.
## [Quick note on module names](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#common-mistakes-1)
**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](#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](#prerequisites-1)
Before creating a page, you need two things:
1. **An entity** – the page needs data to display. We’ll use the `MyModule.Product` entity from the previous steps.
2. **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](#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](#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](#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](#widget-reference)
Here are the most commonly used widgets:
### [Input 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](#display-widgets)
```
DYNAMICTEXT dynName (Attribute: Name)
```
### [Action buttons](#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](#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](#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](#common-mistakes-2)
**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](#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)](#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 (`CRETE` instead of `CREATE`)
- Missing semicolons or parentheses
- Invalid attribute type syntax (`String` without a length)
- Anti-patterns like empty list variables used as loop sources
- Nested loops that should use `RETRIEVE` for 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](#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](#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](#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](#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](#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](#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](#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](#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](#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?](#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](#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](#supported-ai-tools)
mxcli supports six 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 |
| **OpenCode** | `--tool opencode` | `.opencode/`, `opencode.json` | Deep 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 |
| **GitHub Copilot** | (universal) | `AGENTS.md`, `.ai-context/` | Reads AGENTS.md and project context automatically |
| **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](#how-it-works-at-a-glance)
The workflow is the same regardless of which AI tool you use:
1. **Initialize** – `mxcli init` creates config files, skills, and a dev container
2. **Open in dev container** – sandboxes the AI so it only accesses your project
3. **Start the AI assistant** – Claude Code, GitHub Copilot, Cursor, etc.
4. **Ask for changes in natural language** – “Create a Customer entity with name and email”
5. **The AI explores** – it runs SHOW, DESCRIBE, and SEARCH commands via mxcli
6. **The AI writes MDL** – guided by the skill files installed in your project
7. **The AI validates** – `mxcli check` catches syntax errors before anything is applied
8. **The AI executes** – the MDL script is run against your `.mpr` file
9. **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 Integration](#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](#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](#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](#claudemd)
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](#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](#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](#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](#whats-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](#opening-the-dev-container)
1. Open your project folder in VS Code
2. VS Code detects the `.devcontainer/` directory and shows a notification
3. Click **“Reopen in Container”** (or use the command palette: `Dev Containers: Reopen in Container`)
4. Wait for the container to build (first time takes a few minutes)
5. Once inside the container, open a terminal
## [Starting Claude Code](#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](#how-claude-works-with-your-project)
Claude follows a consistent pattern when you ask it to modify your Mendix project:
### [1. Explore](#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](#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](#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](#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](#5-execute)
If validation passes, Claude runs the script against your project:
```
./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'"
```
### [6. Verify](#6-verify)
Claude can run a full project check using the Mendix build tools:
```
./mxcli docker check -p app.mpr
```
## [Example interaction](#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](#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 check`** to catch issues that `mxcli check` alone might miss. The Mendix build tools perform deeper validation.
## [Next steps](#next-steps)
If you use other AI tools alongside Claude Code, see [Other AI tools](#other-ai-tools) (GitHub Copilot, OpenCode, Cursor, Continue.dev, Windsurf). To understand how skills work in detail, see [Skills and CLAUDE.md](#skills-and-claudemd).
# [OpenCode Integration](#opencode-integration)
OpenCode is a fully supported AI integration for mxcli. It gets deep support: a dedicated configuration directory, project-level context in `AGENTS.md`, skill files that teach the AI MDL patterns, slash commands for common operations, and Starlark lint rules.
## [Initializing a project](#initializing-a-project-1)
Use the `--tool opencode` flag:
```
mxcli init --tool opencode /path/to/my-mendix-project
```
To set up both OpenCode and Claude Code together:
```
mxcli init --tool opencode --tool claude /path/to/my-mendix-project
```
## [What gets created](#what-gets-created-1)
After running `mxcli init --tool opencode`, your project directory gains:
```
my-mendix-project/
├── AGENTS.md # Universal AI assistant guide (OpenCode reads this)
├── opencode.json # OpenCode configuration file
├── .opencode/
│ ├── commands/ # Slash commands (/create-entity, etc.)
│ └── skills/ # MDL pattern guides (OpenCode skill format)
│ ├── write-microflows/
│ │ └── SKILL.md
│ ├── create-page/
│ │ └── SKILL.md
│ └── ...
├── .claude/
│ └── lint-rules/ # Starlark lint rules (shared with mxcli lint)
├── .ai-context/
│ ├── skills/ # Shared skill files (universal copy)
│ └── 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)
```
### [opencode.json](#opencodejson)
The `opencode.json` file is OpenCode’s primary configuration. It points to `AGENTS.md` for instructions and to both the OpenCode-format skills in `.opencode/skills/` and the universal skill files in `.ai-context/skills/`:
```
{
"$schema": "https://opencode.ai/config.json",
"instructions": [
"AGENTS.md",
".opencode/skills/**/SKILL.md",
".ai-context/skills/*.md"
]
}
```
### [Skills](#skills-1)
Skills live in `.opencode/skills//SKILL.md` and use YAML frontmatter that OpenCode understands:
```
---
name: write-microflows
description: MDL syntax and patterns for creating microflows
compatibility: opencode
---
# Writing Microflows
...
```
Each skill covers a specific area: microflow syntax, page patterns, security setup, and so on. OpenCode reads the relevant skill before generating MDL, which significantly improves output quality.
### [Commands](#commands-1)
The `.opencode/commands/` directory contains slash commands available inside OpenCode. These mirror the Claude Code commands: `/create-entity`, `/create-microflow`, `/create-page`, `/lint`, and others.
### [Lint rules](#lint-rules)
Lint rules live in `.claude/lint-rules/` regardless of which tool is selected — this is where `mxcli lint` looks for custom Starlark rules. OpenCode init writes the rules there so `mxcli lint` works the same way for both tool choices.
## [Setting up the dev container](#setting-up-the-dev-container-1)
The dev container setup is identical to the Claude Code workflow. Open your project folder in VS Code, click **“Reopen in Container”** when prompted, and wait for the container to build.
### [What’s installed in the container](#whats-installed-in-the-container-1)
| 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 |
## [Starting OpenCode](#starting-opencode)
With the dev container running, open a terminal in VS Code and start OpenCode:
```
opencode
```
OpenCode now has access to your project files, the mxcli binary, the skill files in `.opencode/skills/`, and the commands in `.opencode/commands/`.
## [How OpenCode works with your project](#how-opencode-works-with-your-project)
The workflow mirrors Claude Code exactly:
### [1. Explore](#1-explore-1)
OpenCode uses mxcli commands to understand the 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](#2-read-the-relevant-skill-1)
Before writing MDL, OpenCode reads the appropriate skill file from `.opencode/skills/`. If you ask for a microflow, it reads the `write-microflows` skill. If you ask for a page, it reads `create-page`.
### [3. Write MDL](#3-write-mdl-1)
OpenCode generates an MDL script based on the project context and skill guidance:
```
/** 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](#4-validate-1)
```
./mxcli check script.mdl
./mxcli check script.mdl -p app.mpr --references
```
### [5. Execute](#5-execute-1)
```
./mxcli -p app.mpr -c "EXECUTE SCRIPT 'script.mdl'"
```
### [6. Verify](#6-verify-1)
```
./mxcli docker check -p app.mpr
```
## [Adding OpenCode to an existing project](#adding-opencode-to-an-existing-project)
If you already ran `mxcli init` for another tool and want to add OpenCode support:
```
mxcli add-tool opencode
```
This creates `.opencode/`, `opencode.json`, and the lint rules without touching any existing configuration.
## [Tips for working with OpenCode](#tips-for-working-with-opencode)
- **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 the AI explore first.** OpenCode will run SHOW and DESCRIBE commands to understand what’s already in the project. This leads to better results.
- **Review in Studio Pro.** After changes are applied, open the project in Studio Pro to verify the result visually.
- **Use `mxcli docker check`** to catch issues that `mxcli check` alone might miss.
## [Next steps](#next-steps-1)
To understand what the skill files contain and how they guide AI behavior, see [Skills and CLAUDE.md](#skills-and-claudemd). For other supported tools, see [Other AI tools (Cursor, Continue.dev, Windsurf, OpenCode)](#other-ai-tools).
# [Other AI tools](#other-ai-tools)
Claude Code is the default integration, but mxcli also supports OpenCode, Cursor, Continue.dev, Windsurf, Aider, and Mistral Vibe. Each tool gets its own configuration file that teaches the AI about MDL syntax and mxcli commands.
## [Initializing for a specific tool](#initializing-for-a-specific-tool)
Use the `--tool` flag to specify which AI tool you use:
```
# OpenCode
mxcli init --tool opencode /path/to/my-mendix-project
# 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
# Mistral Vibe
mxcli init --tool vibe /path/to/my-mendix-project
```
## [Setting up multiple tools](#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](#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](#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 |
| **OpenCode** | `.opencode/`, `opencode.json` | Skills, commands, lint rules, project context |
| **GitHub Copilot** | `.github/copilot-instructions.md`, `AGENTS.md` | Project-level instructions auto-loaded by Copilot in VS Code |
| **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 |
| **Mistral Vibe** | `.vibe/` | Config, system prompt, and SKILL.md skills |
## [Tool details](#tool-details)
### [OpenCode](#opencode)
OpenCode receives full integration on par with Claude Code: dedicated skills in `.opencode/skills/`, slash commands in `.opencode/commands/`, and Starlark lint rules in `.claude/lint-rules/`. See the dedicated [OpenCode Integration](#opencode-integration) page for the complete walkthrough.
```
mxcli init --tool opencode /path/to/project
```
### [GitHub Copilot](#github-copilot)
GitHub Copilot has dedicated support via `--tool copilot`, which generates `.github/copilot-instructions.md` — Copilot’s project-level instructions file that’s automatically loaded in VS Code.
```
mxcli init --tool copilot /path/to/project
```
The generated file is compact (Copilot has a smaller instruction context window than Claude Code) and points to `AGENTS.md` and `.ai-context/skills/` for full reference material. It includes the critical `./mxcli` (not `mxcli`) rule, quick command examples, and MDL syntax reminders.
For many organizations, Copilot is the default AI assistant as part of their Microsoft/GitHub enterprise agreement. To use: open the project in VS Code with the GitHub Copilot extension, open Copilot Chat (Ctrl+Shift+I), and ask for Mendix changes. Copilot will reference `.github/copilot-instructions.md`, `AGENTS.md`, and the skill files for MDL syntax guidance.
### [Cursor](#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 conventions
- `AGENTS.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](#continuedev)
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 definitions
- `AGENTS.md` and `.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)
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 reference
- `AGENTS.md` and `.ai-context/skills/` – universal files
To use: open the project in Windsurf, then use Cascade to ask for Mendix changes.
### [Aider](#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 configuration
- `AGENTS.md` and `.ai-context/skills/` – universal files
To use: run `aider` in the project directory from the terminal.
### [Mistral Vibe](#mistral-vibe)
Mistral Vibe is a terminal-based AI coding agent by Mistral AI. It uses `.vibe/config.toml` for configuration, `.vibe/prompts/` for system prompts, and `.vibe/skills/` for SKILL.md-based skills.
```
mxcli init --tool vibe /path/to/project
```
Created files:
- `.vibe/config.toml` – project configuration with system prompt reference
- `.vibe/prompts/mendix-mdl.md` – system prompt with project context and mxcli commands
- `.vibe/skills/write-microflows/SKILL.md` – microflow syntax and rules
- `.vibe/skills/create-page/SKILL.md` – page/widget syntax
- `.vibe/skills/check-syntax/SKILL.md` – validation workflow
- `.vibe/skills/explore-project/SKILL.md` – project query commands
- `AGENTS.md` and `.ai-context/skills/` – universal files
To use: run `vibe` in the project directory from the terminal. Vibe auto-discovers skills in `.vibe/skills/` and loads the system prompt from `.vibe/prompts/`.
## [The universal format: AGENTS.md](#the-universal-format-agentsmd)
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.md`** is 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](#listing-supported-tools)
To see all tools mxcli knows about:
```
mxcli init --list-tools
```
## [Next steps](#next-steps-2)
To understand what the skill files contain and how they guide AI behavior, see [Skills and CLAUDE.md](#skills-and-claudemd).
# [Skills and CLAUDE.md](#skills-and-claudemd)
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](#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](#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](#what-a-skill-file-contains)
A typical skill file has four sections:
### [1. Syntax reference](#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](#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](#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](#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](#claudemd-1)
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 check` before 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](#agentsmd)
`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](#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](#how-the-ai-uses-skills)
The workflow is straightforward:
1. You ask for a change: “Create a page that shows all orders”
2. The AI determines which skill is relevant (in this case, `create-page.md` and `overview-pages.md`)
3. The AI reads the skill files
4. The AI writes MDL following the syntax and patterns in the skill
5. The AI validates with `mxcli check`
6. 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](#next-steps-3)
Now that you understand how skills guide AI behavior, see [The MDL + AI Workflow](#the-mdl--ai-workflow) for the complete recommended workflow from project initialization to review in Studio Pro.
# [The MDL + AI Workflow](#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](#the-ten-step-workflow)
### [Step 1: Initialize the project](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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](#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:**
1. Runs `SHOW MODULES` and `SHOW ENTITIES IN CRM` to understand the project
2. Reads `generate-domain-model.md`, `overview-pages.md`, and `write-microflows.md` skills
3. Writes a script with the entity, enumeration, pages, and microflow
4. Validates with `mxcli check`
5. Executes the script
6. Runs `mxcli docker check` to verify
7. 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](#tips-for-best-results)
### [Be specific about what exists](#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](#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](#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](#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](#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](#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 by Example](#mdl-by-example)
This part presents complete, self-contained MDL examples that show how the language comes together for real use cases. Each example is ready to run against a Mendix project.
Where Part III covers MDL syntax one statement at a time, these examples show full features built end to end – domain model, business logic, pages, and security working together.
## [Examples](#examples)
| Example | What It Shows |
| --- | --- |
| [CRM Module](#crm-module) | Full CRUD feature: entities with documented attributes, associations, validation microflow, overview and edit pages, security |
| [REST Integration](#rest-integration) | Call external APIs from microflows: GET/POST, headers, authentication, error handling, JSON responses |
| [Data Import Pipeline](#data-import-pipeline) | Connect to an external database, explore schema, import data with association linking, generate connectors |
| [Master-Detail Page](#master-detail-page) | Gallery with selection binding, detail form, save/cancel actions |
| [Modifying Existing Pages](#modifying-existing-pages) | ALTER PAGE to add fields, change buttons, replace sections, drop widgets |
| [Validation Pattern](#validation-pattern) | Two-microflow validation with field-level feedback |
| [Role-Based Security](#role-based-security) | Module roles, user roles, entity access with XPath row-level constraints, demo users |
| [View Entities](#view-entities) | OQL-backed aggregation with JOIN, filtered retrieval in microflows |
# [CRM Module](#crm-module)
A complete customer management feature: domain model, validation, CRUD pages, and security – all in one script.
## [Domain Model](#domain-model-1)
```
-- Enumerations first (referenced by entities)
CREATE ENUMERATION CRM.CustomerStatus (
Active 'Active',
Inactive 'Inactive',
Suspended 'Suspended'
);
CREATE ENUMERATION CRM.ContactType (
Email 'Email',
Phone 'Phone',
Visit 'Visit'
);
-- Entities with per-attribute documentation
/** Customer master data */
@Position(100, 100)
CREATE PERSISTENT ENTITY CRM.Customer (
/** Auto-generated unique identifier */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Full legal name */
Name: String(200) NOT NULL ERROR 'Customer name is required',
/** Primary contact email */
Email: String(200) UNIQUE ERROR 'Email already exists',
/** Phone number in international format */
Phone: String(50),
/** Current account balance */
Balance: Decimal DEFAULT 0,
/** Whether the account is active */
IsActive: Boolean DEFAULT TRUE,
/** Current lifecycle status */
Status: Enumeration(CRM.CustomerStatus) DEFAULT 'Active',
/** Free-form notes about this customer */
Notes: String(unlimited)
)
INDEX (Name)
INDEX (Email);
/
/** Record of a customer interaction */
@Position(400, 100)
CREATE PERSISTENT ENTITY CRM.ContactLog (
/** Date and time of the interaction */
ContactDate: DateTime NOT NULL,
/** Type of interaction */
Type: Enumeration(CRM.ContactType) DEFAULT 'Email',
/** Summary of what was discussed */
Summary: String(2000) NOT NULL ERROR 'Summary is required',
/** Follow-up needed? */
FollowUpRequired: Boolean DEFAULT FALSE
);
/
-- Associations
CREATE ASSOCIATION CRM.ContactLog_Customer
FROM CRM.ContactLog TO CRM.Customer
TYPE Reference OWNER Default;
/
```
## [Validation Microflow](#validation-microflow)
The two-microflow pattern: a validation microflow returns field-level feedback, and an action microflow calls it before saving.
```
CREATE MICROFLOW CRM.VAL_Customer ($Customer: CRM.Customer)
RETURNS Boolean AS $IsValid
BEGIN
DECLARE $IsValid Boolean = true;
IF trim($Customer/Name) = '' THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Customer/Name MESSAGE 'Name cannot be empty';
END IF;
IF $Customer/Email != empty AND NOT contains($Customer/Email, '@') THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Enter a valid email address';
END IF;
IF $Customer/Balance < 0 THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Customer/Balance MESSAGE 'Balance cannot be negative';
END IF;
RETURN $IsValid;
END;
/
CREATE MICROFLOW CRM.ACT_Customer_Save ($Customer: CRM.Customer)
RETURNS Boolean AS $IsValid
BEGIN
$IsValid = CALL MICROFLOW CRM.VAL_Customer($param = $Customer);
IF $IsValid THEN
COMMIT $Customer;
CLOSE PAGE;
END IF;
RETURN $IsValid;
END;
/
```
## [Pages](#pages-1)
```
-- Overview page with data grid
CREATE PAGE CRM.Customer_Overview (
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default
) {
DATAGRID2 ON CRM.Customer (
COLUMN Name { Caption: 'Name' }
COLUMN Email { Caption: 'Email' }
COLUMN Phone { Caption: 'Phone' }
COLUMN Status { Caption: 'Status' }
COLUMN IsActive { Caption: 'Active' }
SEARCH ON Name, Email
BUTTON 'New' CALL CRM.Customer_NewEdit
BUTTON 'Edit' CALL CRM.Customer_NewEdit
BUTTON 'Delete' CALL CONFIRM DELETE
)
};
/
-- NewEdit page with validation
CREATE PAGE CRM.Customer_NewEdit (
Params: { $Customer: CRM.Customer },
Title: 'Customer',
Layout: Atlas_Core.PopupLayout
) {
LAYOUTGRID mainGrid {
ROW row1 {
COLUMN col1 (DesktopWidth: AutoFill) {
DATAVIEW dataView1 (DataSource: $Customer) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
FOOTER footer1 {
ACTIONBUTTON btnSave (
Caption: 'Save',
Action: CALL CRM.ACT_Customer_Save,
ButtonStyle: Success
)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
}
}
};
/
```
## [Security](#security-1)
```
-- Module roles
CREATE MODULE ROLE CRM.User;
CREATE MODULE ROLE CRM.Admin DESCRIPTION 'Full customer management access';
-- Entity access
GRANT CRM.Admin ON CRM.Customer (CREATE, DELETE, READ *, WRITE *);
GRANT CRM.User ON CRM.Customer (CREATE, READ *, WRITE *)
WHERE '[IsActive = true]';
GRANT CRM.Admin ON CRM.ContactLog (CREATE, DELETE, READ *, WRITE *);
GRANT CRM.User ON CRM.ContactLog (CREATE, READ *, WRITE *);
-- Document access
GRANT EXECUTE ON MICROFLOW CRM.ACT_Customer_Save TO CRM.User;
GRANT VIEW ON PAGE CRM.Customer_Overview TO CRM.User;
GRANT VIEW ON PAGE CRM.Customer_NewEdit TO CRM.User;
-- User roles
CREATE OR MODIFY USER ROLE CRMUser (System.User, CRM.User);
CREATE OR MODIFY USER ROLE CRMAdmin (System.User, CRM.Admin);
-- Demo users for testing
CREATE OR MODIFY DEMO USER 'crm_user' PASSWORD 'Password1!' (CRMUser);
CREATE OR MODIFY DEMO USER 'crm_admin' PASSWORD 'Password1!' (CRMAdmin);
ALTER PROJECT SECURITY DEMO USERS ON;
```
# [REST Integration](#rest-integration)
Calling external APIs from microflows – GET, POST, authentication, and error handling.
## [Simple GET](#simple-get)
```
CREATE MICROFLOW Integration.FetchWebpage ()
RETURNS String AS $Content
BEGIN
$Content = REST CALL GET 'https://example.com/api/status'
HEADER Accept = 'application/json'
TIMEOUT 30
RETURNS String;
RETURN $Content;
END;
/
```
## [GET with URL Parameters](#get-with-url-parameters)
```
CREATE MICROFLOW Integration.SearchProducts (
$Query: String,
$Page: Integer
)
RETURNS String AS $Response
BEGIN
$Response = REST CALL GET 'https://api.example.com/search?q={1}&page={2}' WITH (
{1} = urlEncode($Query),
{2} = toString($Page)
)
HEADER Accept = 'application/json'
TIMEOUT 60
RETURNS String;
RETURN $Response;
END;
/
```
## [POST with JSON Body](#post-with-json-body)
```
CREATE MICROFLOW Integration.CreateCustomer (
$Name: String,
$Email: String
)
RETURNS String AS $Response
BEGIN
$Response = REST CALL POST 'https://api.example.com/customers'
HEADER 'Content-Type' = 'application/json'
BODY '{{"name": "{1}", "email": "{2}"}' WITH (
{1} = $Name,
{2} = $Email
)
TIMEOUT 30
RETURNS String;
RETURN $Response;
END;
/
```
## [Basic Authentication](#basic-authentication)
```
CREATE MICROFLOW Integration.FetchSecureData (
$Username: String,
$Password: String
)
RETURNS String AS $Response
BEGIN
$Response = REST CALL GET 'https://api.example.com/secure/data'
HEADER Accept = 'application/json'
AUTH BASIC $Username PASSWORD $Password
TIMEOUT 30
RETURNS String;
RETURN $Response;
END;
/
```
## [Error Handling](#error-handling-1)
Use `ON ERROR WITHOUT ROLLBACK` to catch failures and return a fallback instead of rolling back the transaction:
```
CREATE MICROFLOW Integration.SafeAPICall (
$Url: String
)
RETURNS Boolean AS $Success
BEGIN
DECLARE $Success Boolean = false;
$Response = REST CALL GET $Url
HEADER Accept = 'application/json'
TIMEOUT 30
RETURNS String
ON ERROR WITHOUT ROLLBACK {
LOG ERROR NODE 'Integration' 'API call failed: ' + $Url;
RETURN false;
};
SET $Success = true;
RETURN $Success;
END;
/
```
## [JSON Structure + Import Mapping](#json-structure--import-mapping)
Map a JSON response to Mendix entities using a JSON structure and import mapping.
```
-- Step 1: Define the JSON structure
CREATE JSON STRUCTURE Integration.JSON_Pet
SNIPPET '{"id": 1, "name": "Fido", "status": "available"}';
-- Step 2: Create a non-persistent entity to hold the data
CREATE NON-PERSISTENT ENTITY Integration.PetResponse (
PetId: Integer,
Name: String,
Status: String
);
/
-- Step 3: Create the import mapping
CREATE IMPORT MAPPING Integration.IMM_Pet
WITH JSON STRUCTURE Integration.JSON_Pet
{
CREATE Integration.PetResponse {
PetId = id,
Name = name,
Status = status
}
};
-- Step 4: Use in a microflow with IMPORT FROM MAPPING
-- $PetResponse = IMPORT FROM MAPPING Integration.IMM_Pet($JsonContent);
```
## [Export Mapping (Entity → JSON)](#export-mapping-entity--json)
Serialize entities back to JSON using an export mapping.
```
CREATE EXPORT MAPPING Integration.EMM_Pet
WITH JSON STRUCTURE Integration.JSON_Pet
{
Integration.PetResponse {
id = PetId,
name = Name,
status = Status
}
};
-- Use in a microflow with EXPORT TO MAPPING
-- $JsonOutput = EXPORT TO MAPPING Integration.EMM_Pet($PetResponse);
```
## [Nested Mappings with Associations](#nested-mappings-with-associations)
Map nested JSON objects to multiple entities linked by associations.
```
CREATE IMPORT MAPPING Integration.IMM_Order
WITH JSON STRUCTURE Integration.JSON_Order
{
CREATE Integration.OrderResponse {
OrderId = orderId KEY,
CREATE Integration.OrderResponse_CustomerInfo/Integration.CustomerInfo = customer {
Email = email,
Name = name
},
CREATE Integration.OrderResponse_OrderItem/Integration.OrderItem = items {
Sku = sku,
Quantity = quantity,
Price = price
}
}
};
```
## [Consuming a REST API](#consuming-a-rest-api)
### [From an OpenAPI Spec (Recommended)](#from-an-openapi-spec-recommended)
If the API has an OpenAPI 3.0 spec (JSON or YAML), generate the REST client in one command:
```
-- From a local file (relative to the .mpr file)
CREATE OR MODIFY REST CLIENT CapitalModule.CapitalAPI (
OpenAPI: 'specs/capital.json'
);
-- From a URL
CREATE OR MODIFY REST CLIENT PetStoreModule.PetStoreAPI (
OpenAPI: 'https://petstore3.swagger.io/api/v3/openapi.json'
);
```
Operations, path/query parameters, request bodies, response types, resource groups (from `tags`), and Basic auth are all derived automatically from the spec.
**Preview without writing:**
```
DESCRIBE CONTRACT OPERATION FROM OPENAPI 'specs/capital.json';
```
### [Manual Definition](#manual-definition)
Define a reusable REST client with typed operations using `CREATE REST CLIENT`. Each operation declares its method, path, optional parameters, headers, body, and response mapping.
```
-- Define a client for the orders API
CREATE REST CLIENT Integration.OrdersApi (
BaseUrl: 'https://api.example.com/v1',
Authentication: NONE
)
{
OPERATION GetOrder {
Method: GET,
Path: '/orders/{id}',
Parameters: ($id: String),
Headers: ('Accept' = 'application/json'),
Timeout: 30,
Response: JSON AS $Result
}
OPERATION CreateOrder {
Method: POST,
Path: '/orders',
Headers: ('Content-Type' = 'application/json'),
Body: MAPPING Integration.OrderRequest {
customerId = CustomerId,
totalAmount = TotalAmount,
notes = Notes,
},
Response: MAPPING Integration.OrderResponse {
Id = id,
Status = status,
CreatedAt = createdAt,
}
}
};
```
Use `CREATE OR MODIFY REST CLIENT` to update an existing client without dropping it first:
```
CREATE OR MODIFY REST CLIENT Integration.OrdersApi (
BaseUrl: 'https://api.example.com/v2',
Authentication: BASIC (Username: 'apiuser', Password: 'secret')
)
{
OPERATION GetOrder {
Method: GET,
Path: '/orders/{id}',
Parameters: ($id: String),
Headers: ('Accept' = 'application/json'),
Timeout: 60,
Response: JSON AS $Result
}
};
```
**Body types:** `JSON FROM $var`, `TEMPLATE '...'`, `MAPPING Entity { jsonField = Attr, ... }`
**Response types:** `JSON AS $var`, `STRING AS $var`, `FILE AS $var`, `STATUS AS $var`, `NONE`, `MAPPING Entity { Attr = jsonField, ... }`
**Authentication:** `NONE`, `BASIC (Username: '...', Password: '...')`
## [Publishing a REST API](#publishing-a-rest-api)
Create a published REST service with CRUD operations backed by microflows.
```
CREATE PUBLISHED REST SERVICE Module.OrderAPI (
Path: 'rest/orders/v1',
Version: '1.0.0',
ServiceName: 'Order API'
)
{
RESOURCE 'orders' {
GET '' MICROFLOW Module.PRS_GetAllOrders;
GET '{id}' MICROFLOW Module.PRS_GetOrderById;
POST '' MICROFLOW Module.PRS_CreateOrder;
PUT '{id}' MICROFLOW Module.PRS_UpdateOrder;
DELETE '{id}' MICROFLOW Module.PRS_DeleteOrder;
}
};
```
**Operation paths:** Use empty string `''` for the root, `'{paramName}'` for path parameters. Do NOT start or end with `/`. Path parameters must match a microflow parameter name exactly (case-sensitive) — e.g., `'{id}'` requires the microflow to declare `$id: String`.
### [Multiple Resources](#multiple-resources)
```
CREATE PUBLISHED REST SERVICE Module.CrmAPI (
Path: 'rest/crm/v1',
Version: '1.0.0',
ServiceName: 'CRM API'
)
{
RESOURCE 'orders' {
GET '' MICROFLOW Module.PRS_GetOrders;
}
RESOURCE 'customers' {
GET '' MICROFLOW Module.PRS_GetCustomers;
}
RESOURCE 'orders/{orderId}/items' {
GET '' MICROFLOW Module.PRS_GetOrderItems;
}
};
```
### [Update or Remove](#update-or-remove)
```
-- Replace with new version
CREATE OR REPLACE PUBLISHED REST SERVICE Module.OrderAPI (
Path: 'rest/orders/v2',
Version: '2.0.0',
ServiceName: 'Order API v2'
) { ... };
-- Remove entirely
DROP PUBLISHED REST SERVICE Module.OrderAPI;
```
## [Data Transformers (JSLT)](#data-transformers-jslt)
Data Transformers apply transformation steps (JSLT or XSLT) to JSON or XML payloads. Useful for reshaping API responses before import mapping, or normalising data from third-party sources. Requires Mendix 11.9+.
```
-- Create a transformer that extracts key fields from a weather API response.
-- The SOURCE JSON defines a sample payload used for schema inference and testing.
CREATE DATA TRANSFORMER Integration.WeatherTransform
SOURCE JSON '{
"latitude": 51.9,
"longitude": 4.5,
"timezone": "Europe/Amsterdam",
"current": {
"time": "2024-01-15T14:00",
"temperature_2m": 12.8,
"wind_speed_10m": 18.3,
"weather_code": 3
}
}'
{
JSLT $$
{
"lat": .latitude,
"lon": .longitude,
"timezone": .timezone,
"temp": .current.temperature_2m,
"wind_speed": .current.wind_speed_10m,
"code": .current.weather_code
}
$$;
};
-- List all data transformers in the Integration module
LIST DATA TRANSFORMERS IN Integration;
-- Inspect a transformer (outputs a re-executable CREATE statement)
DESCRIBE DATA TRANSFORMER Integration.WeatherTransform;
-- Remove a transformer
DROP DATA TRANSFORMER Integration.WeatherTransform;
```
**Notes:**
- Steps execute in order; the output of each step feeds the next.
- `JSLT '...'` for short single-line expressions; `JSLT $$ ... $$` for multi-line.
- `XSLT $$ ... $$` is also supported for XML-to-XML transformations.
- Requires Mendix 11.9+. Use `SHOW FEATURES` to confirm support before using.
# [Data Import Pipeline](#data-import-pipeline)
Connect to an external database, explore its schema, import data into Mendix, and generate database connectors – all from the mxcli REPL.
## [Connect and Explore](#connect-and-explore)
```
-- Connect to the legacy database
SQL CONNECT postgres 'host=legacy-db port=5432 dbname=crm user=readonly' AS legacy;
-- Discover the schema
SQL legacy SHOW TABLES;
SQL legacy DESCRIBE customers;
SQL legacy DESCRIBE orders;
-- Preview the data
SQL legacy SELECT * FROM customers LIMIT 10;
```
## [Import Data](#import-data)
The `IMPORT FROM` statement reads from the external database and inserts into the Mendix application’s PostgreSQL database with proper Mendix ID generation:
```
-- Import customers
IMPORT FROM legacy
QUERY 'SELECT name, email, phone, active FROM customers'
INTO CRM.Customer
MAP (name AS Name, email AS Email, phone AS Phone, active AS IsActive)
BATCH 500;
-- Import orders with association linking
-- The LINK clause looks up Customer by Email and creates the association
IMPORT FROM legacy
QUERY 'SELECT order_number, order_date, total, customer_email FROM orders'
INTO Sales.Order
MAP (order_number AS OrderNumber, order_date AS OrderDate, total AS TotalAmount)
LINK (customer_email TO Sales.Order_Customer ON Email)
BATCH 500;
```
## [Generate Database Connectors](#generate-database-connectors)
For ongoing integration (querying the external database from microflows at runtime), generate Database Connector entities and queries:
```
-- Auto-generate non-persistent entities and query microflows
SQL legacy GENERATE CONNECTOR INTO Integration
TABLES (customers, orders, products);
```
This generates:
- Constants for JDBC connection string, username, and password
- Non-persistent entities with mapped attributes (SQL types to Mendix types)
- A `DATABASE CONNECTION` definition with query microflows
The output is valid MDL that can be reviewed before execution. Add the `EXEC` flag to execute immediately:
```
SQL legacy GENERATE CONNECTOR INTO Integration
TABLES (customers, orders)
EXEC;
```
## [Execute Queries from Microflows](#execute-queries-from-microflows)
Once a database connection is defined, microflows can execute queries at runtime:
```
-- Non-persistent entity for query results
CREATE NON-PERSISTENT ENTITY Integration.Customer (
/** Customer name from external system */
Name: String(200),
/** Email address */
Email: String(200),
/** Account balance */
Balance: Decimal
);
/
-- Database connection with parameterized query
CREATE DATABASE CONNECTION Integration.LegacyDatabase
TYPE 'PostgreSQL'
CONNECTION STRING @Integration.LegacyDatabase_DBSource
USERNAME @Integration.LegacyDatabase_DBUsername
PASSWORD @Integration.LegacyDatabase_DBPassword
BEGIN
QUERY SearchCustomers
SQL 'SELECT name, email, balance FROM customers WHERE name ILIKE {search}'
PARAMETER search: String DEFAULT '%'
RETURNS Integration.Customer
MAP (name AS Name, email AS Email, balance AS Balance);
END;
/
-- Microflow that executes the query
CREATE MICROFLOW Integration.SearchLegacyCustomers ($SearchTerm: String)
RETURNS List of Integration.Customer AS $Results
BEGIN
$Results = EXECUTE DATABASE QUERY Integration.LegacyDatabase.SearchCustomers
(search = '%' + $SearchTerm + '%');
RETURN $Results;
END;
/
```
# [Master-Detail Page](#master-detail-page)
A single page with a list on the left and a detail form on the right. Selecting an item in the list updates the form via the `SELECTION` data source binding – no microflow needed.
```
CREATE PAGE CRM.Customer_MasterDetail (
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default
) {
LAYOUTGRID mainGrid {
ROW row1 {
-- Master list (left panel)
COLUMN colMaster (DesktopWidth: 4) {
DYNAMICTEXT heading (Content: 'Customers', RenderMode: H3)
GALLERY customerList (DataSource: DATABASE CRM.Customer, Selection: Single) {
TEMPLATE template1 {
DYNAMICTEXT name (
Content: '{1}',
ContentParams: [{1} = Name],
RenderMode: H4
)
DYNAMICTEXT email (
Content: '{1}',
ContentParams: [{1} = Email]
)
}
}
}
-- Detail form (right panel, bound to gallery selection)
COLUMN colDetail (DesktopWidth: 8) {
DYNAMICTEXT detailHeading (Content: 'Details', RenderMode: H3)
DATAVIEW customerDetail (DataSource: SELECTION customerList) {
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
TEXTAREA txtNotes (Label: 'Notes', Attribute: Notes)
FOOTER footer1 {
ACTIONBUTTON btnSave (
Caption: 'Save',
Action: SAVE_CHANGES,
ButtonStyle: Success
)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
}
}
};
/
```
The key pattern is `DataSource: SELECTION customerList` – the data view automatically displays whichever item is selected in the gallery. No event handlers, no microflow calls, no state management.
# [Modifying Existing Pages](#modifying-existing-pages)
`ALTER PAGE` modifies widgets in place without recreating the entire page. This is useful for incremental changes, maintenance, and agent-driven iteration.
## [Change a Button](#change-a-button)
```
ALTER PAGE CRM.Customer_Edit {
SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave
};
```
## [Add a Field After an Existing One](#add-a-field-after-an-existing-one)
```
ALTER PAGE CRM.Customer_Edit {
INSERT AFTER txtEmail {
TEXTBOX txtPhone (Label: 'Phone', Attribute: Phone)
}
};
```
## [Remove Widgets](#remove-widgets)
```
ALTER PAGE CRM.Customer_Edit {
DROP WIDGET txtLegacyField, lblOldNote
};
```
## [Replace an Entire Footer](#replace-an-entire-footer)
```
ALTER PAGE CRM.Customer_Edit {
REPLACE footer1 WITH {
FOOTER newFooter {
ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Success)
ACTIONBUTTON btnDelete (Caption: 'Delete', Action: DELETE, ButtonStyle: Danger)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
};
```
## [Multiple Changes at Once](#multiple-changes-at-once)
```
ALTER PAGE CRM.Customer_Edit {
SET Title = 'Edit Customer Details';
SET Label = 'Email Address' ON txtEmail;
INSERT AFTER txtPhone {
TEXTBOX txtWebsite (Label: 'Website', Attribute: Website)
};
DROP WIDGET lblInternalRef
};
```
## [Add a Page Variable](#add-a-page-variable)
```
ALTER PAGE CRM.ProductOverview {
ADD Variables $showStockColumn: Boolean = 'if (3 < 4) then true else false'
};
```
## [Switch Page Layout](#switch-page-layout)
Change a page’s layout without losing any widgets:
```
-- Switch from TopBar to Default layout (auto-maps by placeholder name)
ALTER PAGE CRM.Customer_Edit {
SET Layout = Atlas_Core.Atlas_Default
};
```
When the new layout has different placeholder names, use `MAP`:
```
ALTER PAGE CRM.Customer_Edit {
SET Layout = Atlas_Core.Atlas_SideBar MAP (Main AS Content, Extra AS Sidebar)
};
```
## [Modify DataGrid Columns](#modify-datagrid-columns)
Target columns using dotted notation `gridName.columnName`:
```
-- Add a column
ALTER PAGE CRM.Customer_List {
INSERT AFTER dgCustomers.Email {
COLUMN Phone (Attribute: Phone, Caption: 'Phone')
}
};
-- Remove a column
ALTER PAGE CRM.Customer_List {
DROP WIDGET dgCustomers.OldNotes
};
-- Rename a column header
ALTER PAGE CRM.Customer_List {
SET Caption = 'E-mail Address' ON dgCustomers.Email
};
```
Use `DESCRIBE PAGE CRM.Customer_List` to discover column names.
## [Works on Snippets Too](#works-on-snippets-too)
```
ALTER SNIPPET CRM.NavigationMenu {
SET Caption = 'Dashboard' ON btnHome;
INSERT AFTER btnHome {
ACTIONBUTTON btnReports (
Caption: 'Reports',
Action: SHOW_PAGE CRM.Reports_Overview
)
}
};
```
# [Validation Pattern](#validation-pattern)
Mendix uses a two-microflow pattern for form validation: a validation microflow checks fields and returns feedback, and an action microflow calls it before saving.
## [The Validation Microflow](#the-validation-microflow)
`VALIDATION FEEDBACK` attaches error messages to specific fields. The form highlights the field and displays the message.
```
CREATE MICROFLOW Sales.VAL_Order ($Order: Sales.Order)
RETURNS Boolean AS $IsValid
BEGIN
DECLARE $IsValid Boolean = true;
-- Required field
IF $Order/OrderNumber = empty OR trim($Order/OrderNumber) = '' THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Order/OrderNumber MESSAGE 'Order number is required';
END IF;
-- Date must be in the future
IF $Order/DeliveryDate != empty AND $Order/DeliveryDate < [%CurrentDateTime%] THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Order/DeliveryDate MESSAGE 'Delivery date must be in the future';
END IF;
-- Numeric range
IF $Order/Quantity <= 0 THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Order/Quantity MESSAGE 'Quantity must be greater than zero';
END IF;
IF $Order/Quantity > 10000 THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Order/Quantity MESSAGE 'Maximum quantity is 10,000';
END IF;
-- Cross-field validation
IF $Order/DiscountPercent > 0 AND $Order/ApprovedBy = empty THEN
SET $IsValid = false;
VALIDATION FEEDBACK $Order/ApprovedBy MESSAGE 'Discounted orders require approval';
END IF;
RETURN $IsValid;
END;
/
```
## [The Action Microflow](#the-action-microflow)
The action microflow calls validation, and only saves if it passes:
```
CREATE MICROFLOW Sales.ACT_Order_Save ($Order: Sales.Order)
RETURNS Boolean AS $IsValid
BEGIN
$IsValid = CALL MICROFLOW Sales.VAL_Order($param = $Order);
IF $IsValid THEN
COMMIT $Order;
CLOSE PAGE;
END IF;
RETURN $IsValid;
END;
/
```
## [Wiring It Up](#wiring-it-up)
The page’s Save button calls the action microflow (not the validation microflow directly):
```
CREATE PAGE Sales.Order_Edit (
Params: { $Order: Sales.Order },
Title: 'Order',
Layout: Atlas_Core.PopupLayout
) {
LAYOUTGRID mainGrid {
ROW row1 {
COLUMN col1 (DesktopWidth: AutoFill) {
DATAVIEW dv (DataSource: $Order) {
TEXTBOX txtOrderNumber (Label: 'Order Number', Attribute: OrderNumber)
DATEPICKER dpDelivery (Label: 'Delivery Date', Attribute: DeliveryDate)
TEXTBOX txtQuantity (Label: 'Quantity', Attribute: Quantity)
TEXTBOX txtDiscount (Label: 'Discount %', Attribute: DiscountPercent)
FOOTER footer1 {
ACTIONBUTTON btnSave (
Caption: 'Save',
Action: CALL Sales.ACT_Order_Save,
ButtonStyle: Success
)
ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES)
}
}
}
}
}
};
/
```
# [Role-Based Security](#role-based-security)
A complete security setup: module roles, entity access with XPath row-level constraints, document access, user roles, and demo users.
## [Module Roles](#module-roles)
Module roles define what actions are available within a module:
```
CREATE MODULE ROLE Sales.Viewer DESCRIPTION 'Read-only access to sales data';
CREATE MODULE ROLE Sales.User DESCRIPTION 'Can create and edit orders';
CREATE MODULE ROLE Sales.Admin DESCRIPTION 'Full access including delete';
```
## [Entity Access](#entity-access)
GRANT controls which CRUD operations a role can perform. XPath constraints in `WHERE` filter which rows are visible:
```
-- Admin: full access to all customers
GRANT Sales.Admin ON Sales.Customer (CREATE, DELETE, READ *, WRITE *);
-- User: can create and edit, but only active customers
GRANT Sales.User ON Sales.Customer (CREATE, READ *, WRITE *)
WHERE '[IsActive = true]';
-- Viewer: read-only, active customers only
GRANT Sales.Viewer ON Sales.Customer (READ *)
WHERE '[IsActive = true]';
-- Orders: users can only see their own (via owner token)
GRANT Sales.User ON Sales.Order (CREATE, READ *, WRITE *)
WHERE '[System.owner = ''[%CurrentUser%]'']';
-- Admin sees all orders
GRANT Sales.Admin ON Sales.Order (CREATE, DELETE, READ *, WRITE *);
```
## [Microflow and Page Access](#microflow-and-page-access)
```
-- Microflow access
GRANT EXECUTE ON MICROFLOW Sales.ACT_Order_Save TO Sales.User;
GRANT EXECUTE ON MICROFLOW Sales.ACT_Order_Delete TO Sales.Admin;
-- Page access
GRANT VIEW ON PAGE Sales.Customer_Overview TO Sales.Viewer;
GRANT VIEW ON PAGE Sales.Customer_Overview TO Sales.User;
GRANT VIEW ON PAGE Sales.Order_Edit TO Sales.User;
GRANT VIEW ON PAGE Sales.Admin_Dashboard TO Sales.Admin;
```
## [User Roles](#user-roles)
User roles combine module roles from different modules into a single assignable role:
```
CREATE OR MODIFY USER ROLE SalesViewer (System.User, Sales.Viewer);
CREATE OR MODIFY USER ROLE SalesRep (System.User, Sales.User);
CREATE OR MODIFY USER ROLE SalesManager (System.User, Sales.Admin) MANAGE ALL ROLES;
```
## [Demo Users](#demo-users)
Demo users are created for testing and development:
```
CREATE OR MODIFY DEMO USER 'viewer' PASSWORD 'Password1!' (SalesViewer);
CREATE OR MODIFY DEMO USER 'sales_rep' PASSWORD 'Password1!' (SalesRep);
CREATE OR MODIFY DEMO USER 'manager' PASSWORD 'Password1!' (SalesManager);
-- Enable demo users in project security
ALTER PROJECT SECURITY DEMO USERS ON;
```
## [Additive Grants](#additive-grants)
GRANT merges with existing access — it never removes permissions:
```
-- Viewer already has READ (Name, Email)
GRANT Sales.Viewer ON Sales.Customer (READ (Phone));
-- Result: READ (Name, Email, Phone)
```
## [Revoking Access](#revoking-access)
```
-- Remove all access for a role
REVOKE Sales.Viewer ON Sales.Customer;
-- Partial revoke: remove read on a specific attribute
REVOKE Sales.User ON Sales.Customer (READ (Phone));
-- Partial revoke: downgrade write to read-only
REVOKE Sales.User ON Sales.Customer (WRITE (Email));
-- Remove microflow access
REVOKE EXECUTE ON MICROFLOW Sales.ACT_Order_Delete FROM Sales.User;
```
# [View Entities](#view-entities)
View entities are read-only entities backed by an OQL query. They appear in the domain model but have no database table – their data is computed from other entities via aggregation and joins.
## [Sales Summary by Category](#sales-summary-by-category)
```
-- Source entities
CREATE PERSISTENT ENTITY Reports.ProductCategory (
/** Category display name */
CategoryName: String(200) NOT NULL
);
/
CREATE PERSISTENT ENTITY Reports.SaleTransaction (
/** Transaction amount */
Amount: Decimal NOT NULL,
/** Date of the sale */
SaleDate: DateTime NOT NULL
);
/
CREATE ASSOCIATION Reports.SaleTransaction_ProductCategory
FROM Reports.ProductCategory TO Reports.SaleTransaction
TYPE Reference OWNER Default;
/
-- View entity: aggregates sales by category
CREATE VIEW ENTITY Reports.SalesTotalByCategory (
CategoryName: String(200),
TotalAmount: Decimal,
TransactionCount: Integer
) AS (
SELECT
c.CategoryName AS CategoryName,
sum(s.Amount) AS TotalAmount,
count(s.ID) AS TransactionCount
FROM Reports.SaleTransaction AS s
INNER JOIN s/Reports.SaleTransaction_ProductCategory/Reports.ProductCategory AS c
GROUP BY c.CategoryName
);
/
```
## [Querying a View Entity in a Microflow](#querying-a-view-entity-in-a-microflow)
View entities can be retrieved like any other entity, including with `WHERE` filters:
```
CREATE MICROFLOW Reports.GetSalesTotalForCategory (
$Category: Reports.ProductCategory
)
RETURNS Decimal AS $TotalAmount
BEGIN
DECLARE $TotalAmount Decimal = 0;
RETRIEVE $Summary FROM Reports.SalesTotalByCategory
WHERE CategoryName = $Category/CategoryName
LIMIT 1;
IF $Summary != empty THEN
SET $TotalAmount = $Summary/TotalAmount;
END IF;
RETURN $TotalAmount;
END;
/
```
## [Displaying in a Page](#displaying-in-a-page)
View entities work with data grids and list views like persistent entities:
```
CREATE PAGE Reports.SalesByCategory_Overview (
Title: 'Sales by Category',
Layout: Atlas_Core.Atlas_Default
) {
DATAGRID2 ON Reports.SalesTotalByCategory (
COLUMN CategoryName { Caption: 'Category' }
COLUMN TotalAmount { Caption: 'Total Sales' }
COLUMN TransactionCount { Caption: 'Transactions' }
)
};
/
```
# [Migration Guide](#migration-guide)
mxcli and MDL enable AI-assisted migration of existing applications to the Mendix platform. An AI coding agent (Claude Code, GitHub Copilot, OpenCode, Cursor, Windsurf) investigates the source application, maps its elements to Mendix concepts, generates MDL scripts, and validates the result – all from the command line.
This part covers the five-phase migration workflow and the skills that support each phase.
## [Why mxcli for Migration?](#why-mxcli-for-migration)
Traditional migrations require deep Mendix expertise and manual work in Studio Pro. With mxcli:
- **AI agents do the heavy lifting** – the agent reads source code, proposes a transformation plan, and generates MDL scripts
- **Skills provide guardrails** – platform-specific migration skills guide the agent with correct mappings, naming conventions, and patterns
- **Validation is automated** – `mxcli check`, `lint`, `docker check`, and `test` catch errors before anyone opens Studio Pro
- **The process is repeatable** – MDL scripts can be version-controlled, reviewed, and re-run
## [The Five Phases](#the-five-phases)
```
Phase 1 Phase 2 Phase 3 Phase 4 Phase 5
┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Assess │──▶│ Propose │──▶│ Generate │──▶│ Test │──▶│ Finish │
│ Source │ │ Transform │ │ Mendix App │ │ & Lint │ │ in Studio│
└──────────┘ └───────────┘ └──────────────┘ └──────────┘ │ Pro │
└──────────┘
```
| Phase | What Happens | Key Skills |
| --- | --- | --- |
| [Assessment](#phase-1-assessment) | Investigate source application, produce inventory | `assess-migration`, platform-specific skills |
| [Transformation](#phase-2-transformation-plan) | Map source elements to Mendix, plan module structure | Assessment output as input |
| [Generation](#phase-3-generation) | Generate domain model, microflows, pages, security | `generate-domain-model`, `write-microflows`, `create-page`, `manage-security` |
| [Data Migration](#data-migration) | Import data from source databases | `demo-data`, `database-connections` |
| [Validation and Handoff](#phase-4-validation-and-handoff) | Test, lint, and hand off to Studio Pro | `check-syntax`, `assess-quality` |
# [Phase 1: Assessment](#phase-1-assessment)
The first step is a thorough investigation of the source application. The AI agent examines the codebase and produces a structured migration inventory.
## [The assess-migration Skill](#the-assess-migration-skill)
The `assess-migration` skill provides a 6-step investigation framework that works with any technology stack – Java, .NET, Python, Node.js, PHP, Ruby, or any other platform. It guides the agent through:
1. **Technology stack** – build files, frameworks, runtime versions
2. **Data model** – ORM entities, database schema, relationships
3. **Business logic** – service classes, stored procedures, validation rules
4. **User interface** – pages, templates, navigation structure
5. **Integrations** – REST/SOAP clients, message queues, file feeds
6. **Security** – authentication, authorization, role definitions
## [What to Extract](#what-to-extract)
| Category | What to Document | Where to Look |
| --- | --- | --- |
| **Data model** | Entities, attributes, relationships, constraints | JPA entities, Django models, EF DbContext, DB schema |
| **Business logic** | Validation rules, calculations, workflows | Service classes, stored procedures, triggers |
| **Pages / UI** | Screens, forms, dashboards, navigation | React components, Razor views, JSP templates |
| **Integrations** | APIs consumed/exposed, file feeds, queues | REST clients, SOAP services, Kafka topics |
| **Security** | Authentication, roles, data access rules | Spring Security, RBAC policies, row-level security |
| **Scheduled jobs** | Background tasks, timers, batch processing | Cron jobs, Quartz schedulers, Celery tasks |
## [Platform-Specific Skills](#platform-specific-skills)
For common source platforms, dedicated skills provide deeper guidance with precise element mappings:
| Source Platform | Skill | Key Mappings |
| --- | --- | --- |
| **Oracle Forms** | `migrate-oracle-forms` | Form -> Page, Block -> Snippet, PL/SQL -> Microflow, LOV -> Enumeration |
| **K2 / Nintex** | `migrate-k2-nintex` | SmartForm -> Page, SmartObject -> Entity, Workflow -> Microflow chain |
| **Any stack** | `assess-migration` | Generic framework for any technology |
## [Assessment Output](#assessment-output)
The assessment produces a structured report with:
- **Executive summary** – technology stack, application size, complexity rating
- **Categorized inventory** – counts and details for each category
- **Mendix mapping** – how each source element maps to Mendix concepts
- **Migration risks** – complex stored procedures, custom UI components, real-time integrations
- **Recommended phases** – suggested order of migration work
## [Example: Starting an Assessment](#example-starting-an-assessment)
Point the AI agent at the source codebase and ask it to assess for migration:
```
Assess the application in /path/to/source for migration to Mendix.
Use the assess-migration skill.
```
The agent will investigate build files, ORM models, service classes, UI templates, security configuration, and API clients. The resulting assessment report becomes the input for Phase 2.
# [Phase 2: Transformation Plan](#phase-2-transformation-plan)
With the assessment complete, the agent creates a detailed transformation plan that maps every source element to its Mendix equivalent.
## [Module Structure](#module-structure)
Plan the Mendix module structure. A common pattern is to consolidate many small source modules into fewer Mendix modules:
```
Source Application Mendix
───────────────── ─────────
CRM_Core, CRM_UI ──▶ CRM
Cases_Core, Cases_UI ──▶ Cases
Auth_Core, Auth_SSO ──▶ Administration
Shared_Utils ──▶ Commons
```
## [Transformation Mapping](#transformation-mapping)
For each element in the assessment, the agent documents the target Mendix implementation:
| Source Element | Source Location | Mendix Target | MDL Statement |
| --- | --- | --- | --- |
| Customer table | `schema.sql` | `CRM.Customer` entity | `CREATE PERSISTENT ENTITY` |
| OrderStatus enum | `enums.py` | `Sales.OrderStatus` enumeration | `CREATE ENUMERATION` |
| calculateTotal() | `OrderService.java` | `Sales.ACT_Order_CalculateTotal` microflow | `CREATE MICROFLOW` |
| Customer list page | `customers.html` | `CRM.Customer_Overview` page | `CREATE PAGE` |
| Admin role | `security.xml` | `Administration.Admin` module role | `GRANT` statements |
## [Prioritization](#prioritization)
Order the migration work to maximize early value and minimize dependency issues:
1. **Enumerations** – no dependencies, used by entities
2. **Domain model** – entities, attributes, associations
3. **Security** – module roles, user roles, access rules
4. **Core business logic** – validation microflows, calculation microflows
5. **Pages** – overview pages, edit forms, dashboards
6. **Integrations** – REST clients, OData services, file handling
7. **Navigation** – menu structure, home pages
8. **Advanced features** – scheduled events, workflows, business events
This order ensures that each layer can reference the elements created before it.
# [Phase 3: Generation](#phase-3-generation)
With the transformation plan in hand, the agent generates the Mendix application using MDL scripts. Skills guide the agent toward correct, idiomatic MDL.
## [Key Skills](#key-skills)
| Skill | Purpose |
| --- | --- |
| `generate-domain-model` | Entity, association, and enumeration syntax with naming conventions |
| `write-microflows` | Microflow syntax, 60+ activity types, common patterns |
| `create-page` | Page and widget syntax for 50+ widget types |
| `overview-pages` | CRUD page patterns (list + detail) |
| `master-detail-pages` | Master-detail page layouts |
| `manage-security` | Module roles, user roles, GRANT/REVOKE, demo users |
| `manage-navigation` | Navigation profiles, menu items, home pages |
| `organize-project` | Folder structure, MOVE command, project conventions |
## [Generation Workflow](#generation-workflow)
```
# 1. Create a new Mendix project in Studio Pro (or use an existing one)
# 2. Execute MDL scripts in dependency order
mxcli exec domain-model.mdl -p app.mpr
mxcli exec microflows.mdl -p app.mpr
mxcli exec pages.mdl -p app.mpr
mxcli exec security.mdl -p app.mpr
# Or work interactively in the REPL
mxcli -p app.mpr
```
## [Example: Domain Model](#example-domain-model)
```
-- Enumerations first (referenced by entities)
CREATE ENUMERATION Sales.OrderStatus (
Draft 'Draft',
Pending 'Pending',
Confirmed 'Confirmed',
Shipped 'Shipped',
Delivered 'Delivered',
Cancelled 'Cancelled'
);
-- Entities
/** Customer master data */
@Position(100, 100)
CREATE PERSISTENT ENTITY CRM.Customer (
Name: String(200) NOT NULL ERROR 'Customer name is required',
Email: String(200) UNIQUE ERROR 'Email already exists',
Phone: String(50),
IsActive: Boolean DEFAULT TRUE
)
INDEX (Name)
INDEX (Email);
/
/** Sales order */
@Position(300, 100)
CREATE PERSISTENT ENTITY Sales.Order (
OrderNumber: String(50) NOT NULL UNIQUE,
OrderDate: DateTime NOT NULL,
TotalAmount: Decimal DEFAULT 0,
Status: Enumeration(Sales.OrderStatus) DEFAULT 'Draft'
)
INDEX (OrderNumber)
INDEX (OrderDate DESC);
/
-- Associations
CREATE ASSOCIATION Sales.Order_Customer
FROM CRM.Customer TO Sales.Order
TYPE Reference OWNER Default;
/
```
## [Example: Microflow](#example-microflow)
```
CREATE MICROFLOW Sales.ACT_Order_CalculateTotal
BEGIN
DECLARE $Order Sales.Order;
RETRIEVE $Lines FROM Sales.OrderLine
WHERE [Sales.OrderLine_Order = $Order];
DECLARE $Total Decimal = 0;
LOOP $Line IN $Lines
BEGIN
SET $Total = $Total + $Line/Price * $Line/Quantity;
END;
CHANGE $Order (TotalAmount = $Total);
COMMIT $Order;
END;
/
```
## [Example: Page](#example-page)
```
CREATE PAGE CRM.Customer_Overview (
Title: 'Customers',
Layout: Atlas_Core.Atlas_Default
) {
DATAGRID2 ON CRM.Customer (
COLUMN Name { Caption: 'Name' }
COLUMN Email { Caption: 'Email' }
COLUMN Phone { Caption: 'Phone' }
COLUMN IsActive { Caption: 'Active' }
SEARCH ON Name, Email
BUTTON 'New' CALL CRM.Customer_NewEdit
BUTTON 'Edit' CALL CRM.Customer_NewEdit
BUTTON 'Delete' CALL CONFIRM DELETE
)
};
/
```
## [Validation Between Steps](#validation-between-steps)
Validate after each script to catch errors early:
```
# Syntax check (fast, no project needed)
mxcli check domain-model.mdl
# Reference validation (checks entity/microflow names exist)
mxcli check pages.mdl -p app.mpr --references
```
# [Data Migration](#data-migration)
mxcli can connect directly to external databases to explore schemas, import data, and generate database connector code. This is useful for migrating reference data, seeding test data, or setting up ongoing integrations with legacy systems.
## [Key Skills](#key-skills-1)
| Skill | Purpose |
| --- | --- |
| `demo-data` | Mendix ID system, association storage, direct PostgreSQL insertion |
| `database-connections` | External database connectivity from microflows (Database Connector module) |
| `patterns-data-processing` | Loop patterns, batch processing, list operations |
## [Connect to the Source Database](#connect-to-the-source-database)
mxcli supports PostgreSQL, Oracle, and SQL Server:
```
-- Connect to the legacy database
SQL CONNECT postgres 'host=legacy-db port=5432 dbname=crm user=readonly password=...' AS legacy;
-- Explore the schema
SQL legacy SHOW TABLES;
SQL legacy DESCRIBE customers;
```
## [Import Data](#import-data-1)
The `IMPORT FROM` statement reads from the source database and inserts into the Mendix application’s PostgreSQL database:
```
-- Import customers
IMPORT FROM legacy
QUERY 'SELECT name, email, phone, active FROM customers'
INTO CRM.Customer
MAP (name AS Name, email AS Email, phone AS Phone, active AS IsActive)
BATCH 500;
-- Import with association linking
IMPORT FROM legacy
QUERY 'SELECT order_number, order_date, total, customer_email FROM orders'
INTO Sales.Order
MAP (order_number AS OrderNumber, order_date AS OrderDate, total AS TotalAmount)
LINK (customer_email TO Sales.Order_Customer ON Email)
BATCH 500;
```
The import pipeline handles:
- **Mendix ID generation** – creates valid 64-bit object IDs
- **Batch insertion** – respects PostgreSQL parameter limits
- **Association linking** – looks up related entities by attribute value
- **Optimistic locking** – sets `MxObjectVersion` if the entity uses it
## [Generate Database Connectors](#generate-database-connectors-1)
For ongoing integration (not one-time import), generate Database Connector entities and microflows:
```
-- Auto-generate non-persistent entities and query microflows
SQL legacy GENERATE CONNECTOR INTO Integration
TABLES (customers, orders, products);
```
This creates:
- Non-persistent entities with mapped attributes
- Constants for connection strings (JDBC URL, username, password)
- `DATABASE CONNECTION` definitions with query microflows
## [Credential Management](#credential-management)
Database credentials should never be hardcoded. mxcli supports:
- **Environment variables** – set `MXCLI_SQL__DSN`
- **YAML config file** – `~/.mxcli/sql.yaml` with per-alias DSN entries
- **Credential isolation** – credentials are never exposed to the AI agent or logged
# [Phase 4: Validation and Handoff](#phase-4-validation-and-handoff)
mxcli provides a multi-level validation pipeline. The agent uses these tools to self-correct before a human ever looks at the result.
## [Validation Steps](#validation-steps)
```
# 1. Syntax check (fast, no project needed)
mxcli check script.mdl
# 2. Reference validation (checks entity/microflow names exist)
mxcli check script.mdl -p app.mpr --references
# 3. Lint the full project (41 built-in + 27 Starlark rules)
mxcli lint -p app.mpr
# 4. Quality report (scored 0-100 per category)
mxcli report -p app.mpr --format markdown
# 5. Mendix compiler check (requires Docker)
mxcli docker check -p app.mpr
# 6. Build and run (requires Docker)
mxcli docker run -p app.mpr
```
## [Lint Categories](#lint-categories)
The linter covers 6 categories:
| Category | Focus | Example Rules |
| --- | --- | --- |
| **Naming** (CONV) | Naming conventions | Entity naming, microflow prefixes |
| **Security** (SEC) | Access control | Missing access rules, open security |
| **Quality** (QUAL) | Code quality | Unused variables, empty microflows |
| **Architecture** (ARCH) | Structure | Module dependencies, circular references |
| **Performance** (DESIGN) | Efficiency | Missing indexes, large retrieve-all |
| **Design** (MDL) | Best practices | Entity design, association patterns |
## [Quality Reports](#quality-reports)
The `assess-quality` skill guides the agent to run `mxcli report` and interpret the scores. A migration is ready for handoff when:
- No critical lint issues (SEC, QUAL categories)
- Quality score above 70 across all categories
- `mxcli docker check` passes with no errors
- All tests pass
## [Automated Testing](#automated-testing)
Write `.test.mdl` files to validate migrated business logic:
```
-- tests/order_tests.test.mdl
-- @test: Order total is calculated correctly
$Customer = CREATE CRM.Customer (Name = 'Test');
COMMIT $Customer;
$Order = CREATE Sales.Order (OrderNumber = 'ORD-001', OrderDate = now());
CHANGE $Order (Sales.Order_Customer = $Customer);
COMMIT $Order;
$Line = CREATE Sales.OrderLine (Price = 10.00, Quantity = 3);
CHANGE $Line (Sales.OrderLine_Order = $Order);
COMMIT $Line;
CALL MICROFLOW Sales.ACT_Order_CalculateTotal ($Order = $Order);
-- @assert: $Order/TotalAmount = 30.00
```
```
# Run tests (requires Docker)
mxcli test tests/ -p app.mpr
```
## [Handoff to Studio Pro](#handoff-to-studio-pro)
The final step transitions to Mendix Studio Pro for visual refinement:
| Task | Why Studio Pro |
| --- | --- |
| **Page layout tuning** | Visual drag-and-drop for pixel-perfect layouts |
| **Styling and theming** | CSS/SCSS editing with live preview |
| **Complex workflows** | Workflow editor for multi-step approval processes |
| **Marketplace modules** | Install and configure marketplace modules |
| **Deployment** | Configure deployment pipelines and environments |
### [Handoff Checklist](#handoff-checklist)
```
# Final validation before opening in Studio Pro
mxcli docker check -p app.mpr # No compiler errors
mxcli lint -p app.mpr # No critical lint issues
mxcli report -p app.mpr # Review quality scores
mxcli test tests/ -p app.mpr # All tests pass
mxcli -p app.mpr -c "SHOW STRUCTURE DEPTH 2" # Review structure
```
## [Iterative Workflow](#iterative-workflow)
The mxcli/Studio Pro workflow is iterative – you can switch between them:
```
mxcli/MDL Studio Pro
┌──────────┐ ┌──────────────┐
│ Generate │──── open ───▶│ Visual edit │
│ entities, │ │ styling, │
│ microflows│◀── save ─────│ test, deploy │
│ pages │ │ │
└──────────┘ └──────────────┘
```
Changes made in either tool are persisted in the `.mpr` file. Always close mxcli before opening in Studio Pro, and vice versa.
# [MDL Basics](#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](#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](#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](#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](#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](#further-reading)
- [Lexical Structure](#lexical-structure) – keywords, literals, and tokens
- [Qualified Names](#qualified-names) – how elements are referenced
- [Comments](#comments-and-documentation) – comment syntax
- [Script Files](#script-files) – running MDL from files
# [Lexical Structure](#lexical-structure)
This page describes the tokens that make up the MDL language: keywords, literals, and identifiers.
## [Keywords](#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](#literals)
### [String 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](#numeric-literals)
```
42 -- Integer
3.14 -- Decimal
-100 -- Negative integer
1.5e10 -- Scientific notation
```
### [Boolean Literals](#boolean-literals)
```
TRUE
FALSE
```
## [Quoted Identifiers](#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](#qualified-names) for more on identifier syntax and the `Module.Name` notation.
# [Qualified Names](#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](#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](#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](#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](#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](#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](#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](#see-also)
- [Lexical Structure](#lexical-structure) – keywords and quoting
- [Entities](#entities-1) – entity qualified names
- [Associations](#associations-1) – association naming conventions
# [Comments and Documentation](#comments-and-documentation)
MDL supports three comment styles for annotating scripts and attaching documentation to model elements.
## [Single-Line Comments](#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](#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](#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](#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](#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](#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;
```
## [Execution Modes](#execution-modes)
mxcli supports several ways to execute MDL. All modes open a single connection to the project — there is no per-command overhead.
### [Script File](#script-file)
Use `mxcli exec` to run a `.mdl` file:
```
mxcli exec setup_domain_model.mdl -p /path/to/app.mpr
```
### [Inline Commands (`-c`)](#inline-commands--c)
Pass one or more semicolon-separated commands with the `-c` flag:
```
# Single command
mxcli -p app.mpr -c "LIST ENTITIES"
# Multiple commands in one connection
mxcli -p app.mpr -c "DESCRIBE ENTITY Sales.Customer; DESCRIBE ENTITY Sales.Order; LIST MICROFLOWS IN Sales"
```
This is the fastest way for AI agents to batch multiple queries — all commands share a single connection.
### [Stdin Piping](#stdin-piping)
When stdin is a pipe (not a terminal), mxcli reads commands from it in quiet mode (no banner, no prompts):
```
# Pipe from echo
echo "LIST ENTITIES; LIST MICROFLOWS" | mxcli -p app.mpr
# Pipe from file
mxcli -p app.mpr < commands.mdl
# Pipe from heredoc
mxcli -p app.mpr <<'EOF'
DESCRIBE ENTITY Sales.Customer;
DESCRIBE ENTITY Sales.Order;
LIST MICROFLOWS IN Sales;
EOF
```
### [From the REPL](#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](#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](#comments-in-scripts)
Scripts support the same [comment syntax](#comments-and-documentation) as interactive MDL:
```
-- Single-line comment
/* Multi-line comment */
/** Documentation comment (attached to next element) */
```
# [Data Types](#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](#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](#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](#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)` to `String(200)` – increasing length
- `Integer` to `Long` – widening numeric type
Incompatible type changes:
- `String` to `Integer`
- `Boolean` to `String`
- Any type to `AutoNumber` (if data exists)
## [Further Reading](#further-reading-1)
- [Primitive Types](#primitive-types) – detailed reference for each type
- [Constraints](#constraints) – NOT NULL, UNIQUE, DEFAULT
- [Type Mapping](#type-mapping) – MDL to Mendix to database mapping
- [Enumerations](#enumerations) – enumeration type definitions
# [Primitive Types](#primitive-types)
This page documents each primitive type available for entity attributes in MDL.
## [String](#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](#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](#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](#decimal)
High-precision decimal number with up to 20 digits total.
```
Decimal
```
```
Price: Decimal
Amount: Decimal DEFAULT 0
TaxRate: Decimal DEFAULT 0.21
```
## [Boolean](#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](#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)
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](#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](#constraints).
## [Binary](#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](#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](#enumeration-reference)
References an enumeration type defined with `CREATE ENUMERATION`. See [Enumerations](#enumerations) for details.
```
Enumeration()
```
```
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](#see-also-1)
- [Data Types](#data-types) – type system overview
- [Constraints](#constraints) – NOT NULL, UNIQUE, DEFAULT
- [Type Mapping](#type-mapping) – MDL to Mendix to database mapping
# [Constraints](#constraints)
Constraints restrict the values an attribute can hold. They are specified after the type in an attribute definition.
## [Syntax](#syntax)
```
: [NOT NULL [ERROR '']] [UNIQUE [ERROR '']] [DEFAULT ]
```
Constraints must appear in this order:
1. `NOT NULL` (optionally with `ERROR`)
2. `UNIQUE` (optionally with `ERROR`)
3. `DEFAULT`
## [NOT NULL](#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](#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](#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](#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](#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](#see-also-2)
- [Primitive Types](#primitive-types) – type reference
- [Attributes](#attributes-and-validation-rules) – full attribute definition syntax
- [ALTER ENTITY](#alter-entity) – modifying constraints on existing entities
# [Enumerations](#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](#create-enumeration)
```
[/** */]
CREATE ENUMERATION . (
'' [, ...]
)
```
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](#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](#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](#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](#see-also-3)
- [Primitive Types](#primitive-types) – all attribute types including enumeration references
- [Data Types](#data-types) – type system overview
- [Entities](#entities-1) – using enumerations in entity definitions
# [Type Mapping](#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-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-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: }` |
All default values are serialized as strings in the BSON storage, regardless of the attribute type.
## [See Also](#see-also-4)
- [Primitive Types](#primitive-types) – MDL type syntax and usage
- [Data Types](#data-types) – type system overview
# [Domain Model](#domain-model-2)
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](#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](#module-scope)
Every domain model element belongs to a module. Elements are always referenced by their [qualified name](#qualified-names) in `Module.Element` format:
```
Sales.Customer -- entity
Sales.OrderStatus -- enumeration
Sales.Order_Customer -- association
```
## [Complete Example](#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](#further-reading-2)
- [Entities](#entities-1) – entity types and CREATE ENTITY syntax
- [Attributes](#attributes-and-validation-rules) – attribute definitions and validation
- [Associations](#associations-1) – relationships between entities
- [Generalization](#generalization-1) – entity inheritance
- [Indexes](#indexes) – index creation
- [Enumerations](#enumerations) – enumeration types
- [ALTER ENTITY](#alter-entity) – modifying existing entities
# [Entities](#entities-1)
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](#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](#create-entity)
```
[/** */]
[@Position(, )]
CREATE [OR MODIFY] ENTITY . (
)
[INDEX ()]
[ON BEFORE|AFTER CREATE|COMMIT|DELETE|ROLLBACK CALL . [RAISE ERROR]]
[;|/]
```
### [Persistent Entity](#persistent-entity)
The most common type. Data is stored in the application database:
```
/** Customer master data */
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
/** Auto-incrementing unique identifier */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Full legal name of the customer */
Name: String(200) NOT NULL ERROR 'Name is required',
/** Primary contact email address */
Email: String(200) UNIQUE ERROR 'Email must be unique',
/** Current account balance */
Balance: Decimal DEFAULT 0,
/** Whether the account is active */
IsActive: Boolean DEFAULT TRUE,
/** Timestamp of account creation */
CreatedDate: DateTime,
/** Current lifecycle status */
Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name)
INDEX (Email);
```
### [Non-Persistent Entity](#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](#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](#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
);
```
## [System Attributes (Auditing)](#system-attributes-auditing)
Persistent entities can track who created/modified objects and when. Declare them as regular attributes using pseudo-types (like `AutoNumber`):
```
CREATE PERSISTENT ENTITY Sales.Order (
OrderNumber: AutoNumber,
TotalAmount: Decimal,
Owner: AutoOwner,
ChangedBy: AutoChangedBy,
CreatedDate: AutoCreatedDate,
ChangedDate: AutoChangedDate
);
```
| Pseudo-Type | System Attribute | Set When |
| --- | --- | --- |
| `AutoOwner` | `System.owner` (→ System.User) | Object created |
| `AutoChangedBy` | `System.changedBy` (→ System.User) | Every commit |
| `AutoCreatedDate` | `CreatedDate` (DateTime) | Object created |
| `AutoChangedDate` | `ChangedDate` (DateTime) | Every commit |
Toggle on existing entities with ALTER ENTITY:
```
ALTER ENTITY Sales.Order ADD ATTRIBUTE Owner: AutoOwner;
ALTER ENTITY Sales.Order DROP ATTRIBUTE ChangedDate;
```
## [Annotations](#annotations)
### [@Position](#position)
Controls where the entity appears in the domain model diagram:
```
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer ( ... );
```
### [Documentation](#documentation)
A `/** ... */` comment before the entity or before an individual attribute becomes its documentation in Studio Pro:
```
/** Customer master data.
* Stores both active and inactive customers.
*/
@Position(100, 200)
CREATE PERSISTENT ENTITY Sales.Customer (
/** Auto-incrementing unique identifier */
CustomerId: AutoNumber NOT NULL UNIQUE DEFAULT 1,
/** Full legal name of the customer */
Name: String(200) NOT NULL ERROR 'Name is required',
/** Primary contact email address */
Email: String(200) UNIQUE ERROR 'Email must be unique',
/** Current account balance in the base currency */
Balance: Decimal DEFAULT 0,
/** Whether the customer account is active */
IsActive: Boolean DEFAULT TRUE,
/** Timestamp of account creation */
CreatedDate: DateTime,
/** Current lifecycle status */
Status: Enumeration(Sales.CustomerStatus) DEFAULT 'Active'
)
INDEX (Name)
INDEX (Email);
```
Attribute-level documentation appears in Studio Pro when hovering over the attribute in the domain model.
## [Entity Event Handlers](#entity-event-handlers)
Persistent entities can have microflow event handlers that run before or after Create, Commit, Delete, or Rollback operations. The optional `RAISE ERROR` clause makes the handler act as a validation microflow — if it returns false, the operation is aborted.
```
CREATE PERSISTENT ENTITY Sales.Order (
Total: Decimal,
Status: String(50)
)
ON BEFORE COMMIT CALL Sales.ACT_ValidateOrder RAISE ERROR
ON AFTER CREATE CALL Sales.ACT_InitDefaults;
```
Event handlers can also be added or removed via `ALTER ENTITY`:
```
-- Add a handler to an existing entity
ALTER ENTITY Sales.Order
ADD EVENT HANDLER ON BEFORE DELETE CALL Sales.ACT_CheckCanDelete RAISE ERROR;
-- Remove a handler
ALTER ENTITY Sales.Order
DROP EVENT HANDLER ON BEFORE COMMIT;
```
**Moments**: `BEFORE`, `AFTER`
**Events**: `CREATE`, `COMMIT`, `DELETE`, `ROLLBACK`
Each (Moment, Event) combination supports one handler per entity. The microflow must exist in the project (validated at execution time).
## [DROP ENTITY](#drop-entity)
Removes an entity from the domain model:
```
DROP ENTITY Sales.Customer;
```
## [See Also](#see-also-5)
- [Attributes](#attributes-and-validation-rules) – attribute definitions within entities
- [Indexes](#indexes) – adding indexes to entities
- [Generalization](#generalization-1) – entity inheritance with EXTENDS
- [ALTER ENTITY](#alter-entity) – modifying existing entities
- [Associations](#associations-1) – relationships between entities
# [Attributes and Validation Rules](#attributes-and-validation-rules)
Attributes define the fields on an entity. Each attribute has a name, a type, and optional constraints.
## [Attribute Syntax](#attribute-syntax)
```
[/** */]
: [NOT NULL [ERROR '']] [UNIQUE [ERROR '']] [DEFAULT ]
```
| Component | Description |
| --- | --- |
| Name | Attribute identifier (follows [identifier rules](#qualified-names)) |
| Type | One of the [primitive types](#primitive-types) or an [enumeration](#enumerations) reference |
| `NOT NULL` | Value is required |
| `UNIQUE` | Value must be unique across all objects |
| `DEFAULT` | Initial value on object creation |
| `/** ... */` | Documentation comment |
## [Attribute Ordering](#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)
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](#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](#documentation-comments-1)
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](#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](#see-also-6)
- [Primitive Types](#primitive-types) – all available attribute types
- [Constraints](#constraints) – NOT NULL, UNIQUE, DEFAULT in detail
- [Entities](#entities-1) – entity definitions that contain attributes
- [ALTER ENTITY](#alter-entity) – adding and modifying attributes on existing entities
# [Associations](#associations-1)
Associations define relationships between entities. They determine how objects reference each other and control behavior on deletion.
## [Association Types](#association-types-1)
| 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](#create-association)
```
[/** */]
CREATE ASSOCIATION .
FROM
TO
TYPE
[OWNER ]
[DELETE_BEHAVIOR ]
```
### [Reference (Many-to-One)](#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)](#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](#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](#delete-behavior-1)
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](#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](#drop-association)
Remove an association:
```
DROP ASSOCIATION Sales.Order_Customer;
```
## [See Also](#see-also-7)
- [Domain Model](#domain-model-2) – overview of domain model concepts
- [Entities](#entities-1) – the entities that associations connect
- [Generalization](#generalization-1) – inheritance (a different kind of relationship)
# [Generalization](#generalization-1)
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](#syntax-1)
```
CREATE PERSISTENT ENTITY .
EXTENDS
(
);
```
Both `EXTENDS` (preferred) and `GENERALIZATION` (legacy) keywords are supported.
## [Examples](#examples-1)
### [Extending System.User](#extending-systemuser)
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](#extending-systemimage)
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](#extending-systemfiledocument)
For entities that store file attachments:
```
/** File attachment entity */
CREATE PERSISTENT ENTITY Docs.Attachment EXTENDS System.FileDocument (
Description: String(500)
);
```
## [Common System Generalizations](#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](#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](#see-also-8)
- [Entities](#entities-1) – entity types and CREATE ENTITY syntax
- [Associations](#associations-1) – relationships between entities
- [Domain Model](#domain-model-2) – domain model overview
# [Indexes](#indexes)
Indexes improve query performance for frequently searched or sorted attributes. They are defined after the attribute list in an entity definition.
## [Syntax](#syntax-2)
```
INDEX ( [ASC|DESC] [, [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](#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](#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](#guidelines)
1. **Primary lookups** – index columns used in WHERE clauses
2. **Foreign keys** – index columns used in association joins
3. **Sorting** – index columns used in ORDER BY clauses
4. **Composite order** – put high-selectivity columns first in composite indexes
## [See Also](#see-also-9)
- [Entities](#entities-1) – entity definitions that contain indexes
- [ALTER ENTITY](#alter-entity) – adding/removing indexes on existing entities
# [ALTER ENTITY](#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-attributes)
Add one or more new attributes:
```
ALTER ENTITY Sales.Customer
ADD (Phone: String(50), Notes: String(unlimited));
```
New attributes support the same [constraints](#constraints) as in `CREATE ENTITY`:
```
ALTER ENTITY Sales.Customer
ADD (
LoyaltyPoints: Integer DEFAULT 0,
MemberSince: DateTime NOT NULL
);
```
## [DROP Attributes](#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](#modify-attributes)
Change the type or constraints of existing attributes:
```
ALTER ENTITY Sales.Customer
MODIFY (Name: String(400) NOT NULL);
```
## [RENAME Attributes](#rename-attributes)
Rename an attribute:
```
ALTER ENTITY Sales.Customer
RENAME Phone TO PhoneNumber;
```
## [ADD INDEX](#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](#drop-index)
Remove an index:
```
ALTER ENTITY Sales.Customer
DROP INDEX (Email);
```
## [SET DOCUMENTATION](#set-documentation)
Update the entity’s documentation text:
```
ALTER ENTITY Sales.Customer
SET DOCUMENTATION 'Customer master data for the Sales module';
```
## [ADD/DROP System Attributes](#adddrop-system-attributes)
System attributes use the same ADD/DROP syntax as regular attributes:
```
-- Add system attributes
ALTER ENTITY Sales.Order ADD ATTRIBUTE Owner: AutoOwner;
ALTER ENTITY Sales.Order ADD ATTRIBUTE ChangedBy: AutoChangedBy;
ALTER ENTITY Sales.Order ADD ATTRIBUTE CreatedDate: AutoCreatedDate;
ALTER ENTITY Sales.Order ADD ATTRIBUTE ChangedDate: AutoChangedDate;
-- Drop system attributes (by name)
ALTER ENTITY Sales.Order DROP ATTRIBUTE Owner;
ALTER ENTITY Sales.Order DROP ATTRIBUTE ChangedDate;
```
## [ADD/DROP EVENT HANDLER](#adddrop-event-handler)
Register microflows to run before or after entity operations:
```
-- Before commit: validates and can abort (RAISE ERROR)
ALTER ENTITY Sales.Order
ADD EVENT HANDLER ON BEFORE COMMIT CALL Sales.ValidateOrder($currentObject) RAISE ERROR;
-- After commit: runs after successful commit (no RAISE ERROR)
ALTER ENTITY Sales.Order
ADD EVENT HANDLER ON AFTER COMMIT CALL Sales.LogOrderChange($currentObject);
-- Without passing the entity object
ALTER ENTITY Sales.Order
ADD EVENT HANDLER ON AFTER CREATE CALL Sales.NotifyNewOrder();
-- Remove an event handler
ALTER ENTITY Sales.Order
DROP EVENT HANDLER ON BEFORE COMMIT;
```
| Moment | Returns | RAISE ERROR | Use case |
| --- | --- | --- | --- |
| `BEFORE` | Boolean | Yes — aborts on `false` | Validation, permission checks |
| `AFTER` | Void | No | Logging, notifications, side effects |
Events: `CREATE`, `COMMIT`, `DELETE`, `ROLLBACK`
Parameter: `($currentObject)` passes the entity to the microflow, `()` does not.
## [Syntax Summary](#syntax-summary)
```
ALTER ENTITY .
ADD ( [, ...])
ALTER ENTITY .
DROP ( [, ...])
ALTER ENTITY .
MODIFY ( [, ...])
ALTER ENTITY .
RENAME TO
ALTER ENTITY .
ADD INDEX ()
ALTER ENTITY .
DROP INDEX ()
ALTER ENTITY .
SET DOCUMENTATION ''
ALTER ENTITY .
SET POSITION (, )
```
## [See Also](#see-also-10)
- [Entities](#entities-1) – CREATE ENTITY syntax
- [Attributes](#attributes-and-validation-rules) – attribute definition format
- [Indexes](#indexes) – index creation and management
- [Constraints](#constraints) – NOT NULL, UNIQUE, DEFAULT
# [Microflows and Nanoflows](#microflows-and-nanoflows-1)
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?](#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?](#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](#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](#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](#drop)
Remove a microflow or nanoflow:
```
DROP MICROFLOW MyModule.ACT_CreateOrder;
DROP NANOFLOW MyModule.NAV_ShowDetails;
```
## [OR REPLACE](#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](#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](#quick-example-1)
```
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](#next-steps-4)
- [Structure](#structure) – parameters, variables, return types, and the full `CREATE MICROFLOW` syntax
- [Activity Types](#activity-types) – all available activities (RETRIEVE, CREATE, CHANGE, COMMIT, DELETE, CALL, LOG, etc.)
- [Control Flow](#control-flow) – IF/ELSE, LOOP, WHILE, error handling
- [Expressions](#expressions) – expression syntax for conditions and calculations
- [Nanoflows vs Microflows](#nanoflows-vs-microflows) – differences and nanoflow-specific syntax
- [Common Patterns](#common-patterns) – CRUD, validation, batch processing, and anti-patterns to avoid
# [Structure](#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](#full-create-microflow-syntax)
```
CREATE [OR REPLACE] MICROFLOW
[FOLDER '']
BEGIN
[]
[]
[RETURN ;]
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)
Parameters are declared with `DECLARE` at the top of the `BEGIN...END` block. They define the inputs to the microflow.
### [Primitive Parameters](#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)
Entity parameters receive a single object:
```
DECLARE $Customer MyModule.Customer;
```
> **Important:** Do not use `= empty` or `AS` with entity declarations. The correct syntax is simply `DECLARE $Var Module.Entity;`.
### [List Parameters](#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](#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](#variables-and-assignment)
### [Variable Declaration](#variable-declaration)
```
DECLARE $Message String = 'Hello';
DECLARE $Total Decimal = 0;
DECLARE $Found Boolean = false;
```
### [Assignment with SET](#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](#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](#return-values)
Every flow path should end with a `RETURN` statement. The return type is inferred from the returned value.
### [Returning a Primitive](#returning-a-primitive)
```
RETURN true;
RETURN $Total;
RETURN 'Success';
```
### [Returning an Object](#returning-an-object)
```
RETURN $Order;
```
### [Returning a List](#returning-a-list)
```
RETURN $FilteredOrders;
```
### [Returning Nothing](#returning-nothing)
If the microflow returns nothing (void), you can omit the `RETURN` or use:
```
RETURN;
```
## [Annotations](#annotations-1)
Annotations are metadata decorators placed **before** an activity. They control visual layout and documentation in Mendix Studio Pro.
### [Position](#position-1)
Set the canvas position of the next activity:
```
@position(200, 100)
$Order = CREATE Sales.Order (Status = 'New');
```
### [Caption](#caption)
Set a custom caption displayed on the activity in the canvas:
```
@caption 'Create new order'
$Order = CREATE Sales.Order (Status = 'New');
```
### [Color](#color)
Set the background color of the activity:
```
@color Green
COMMIT $Order;
```
### [Annotation (Visual Note)](#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](#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](#complete-example-1)
```
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](#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](#object-operations)
### [CREATE](#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](#change)
Modifies attributes of an existing object:
```
CHANGE $Order (
Status = 'Confirmed',
TotalAmount = $CalculatedTotal
);
```
Multiple attributes can be changed in a single `CHANGE` statement.
### [COMMIT](#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](#delete)
Deletes an object from the database:
```
DELETE $Order;
```
### [ROLLBACK](#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](#retrieval)
### [RETRIEVE from Database (XPath)](#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](#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](#retrieve-from-variable-list)
Retrieves from an in-memory list variable:
```
RETRIEVE $Match FROM $CustomerList
WHERE Email = $SearchEmail
LIMIT 1;
```
## [Call Activities](#call-activities)
### [CALL MICROFLOW](#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](#call-nanoflow)
Calls a nanoflow from within a microflow:
```
$Result = CALL NANOFLOW MyModule.ValidateInput (
InputValue = $Value
);
```
### [CALL JAVA ACTION](#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](#ui-activities)
### [SHOW PAGE](#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](#close-page)
Closes the current page:
```
CLOSE PAGE;
```
## [Validation](#validation)
### [VALIDATION FEEDBACK](#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](#logging)
### [LOG](#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](#database-query-execution)
### [EXECUTE DATABASE QUERY](#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 on `EXECUTE DATABASE QUERY` activities.
## [Summary Table](#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](#control-flow)
MDL microflows support conditional branching, loops, and error handling to control the execution path of your logic.
## [IF / ELSE](#if--else)
Conditional branching executes different activities based on a boolean expression.
### [Basic IF](#basic-if)
```
IF $Order/TotalAmount > 1000 THEN
CHANGE $Order (DiscountApplied = true);
END IF;
```
### [IF / ELSE](#if--else-1)
```
IF $Customer/Email != empty THEN
CALL MICROFLOW Sales.SUB_SendEmail (Customer = $Customer);
ELSE
LOG WARNING 'Customer has no email address';
END IF;
```
### [Nested 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](#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)](#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-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](#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](#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](#error-handling-2)
### [ON ERROR Suffix](#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](#on-error-continue)
Ignores the error and continues to the next activity:
```
COMMIT $Order ON ERROR CONTINUE;
```
#### [ON ERROR ROLLBACK](#on-error-rollback)
Rolls back the current transaction and continues:
```
DELETE $Order ON ERROR ROLLBACK;
```
#### [ON ERROR with Handler Block](#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](#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 ERROR` is not supported on `EXECUTE DATABASE QUERY` activities.
## [Unsupported Control Flow](#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](#complete-example-2)
```
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)
Expressions in MDL are used in conditions, attribute assignments, variable assignments, and log messages within microflows. They follow Mendix expression syntax.
## [Literals](#literals-1)
### [String Literals](#string-literals-1)
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](#numeric-literals-1)
```
42 -- Integer
3.14 -- Decimal
-100 -- Negative integer
1.5e10 -- Scientific notation
```
### [Boolean Literals](#boolean-literals-1)
```
true
false
```
### [Empty](#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](#operators)
### [Arithmetic 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](#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](#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](#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](#datetime-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-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](#type-conversion)
Use built-in functions for type conversion in expressions:
```
toString($IntValue) -- Integer/Decimal/Boolean to String
```
## [Enumeration Values](#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](#expression-contexts)
Expressions appear in several places within microflow activities:
### [In Conditions (IF, WHILE, WHERE)](#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)](#in-attribute-assignments-create-change)
```
$Order = CREATE Sales.Order (
OrderDate = [%CurrentDateTime%],
Description = 'Order for ' + $Customer/Name,
TotalAmount = $Subtotal + $Tax
);
```
### [In SET Assignments](#in-set-assignments)
```
SET $Counter = $Counter + 1;
SET $IsEligible = $Customer/Age >= 18 AND $Customer/IsActive;
SET $FullName = $Customer/FirstName + ' ' + $Customer/LastName;
```
### [In LOG Messages](#in-log-messages)
```
LOG INFO 'Processed ' + toString($Count) + ' orders';
LOG ERROR NODE 'Validation' 'Invalid amount: ' + toString($Order/TotalAmount);
```
### [In VALIDATION FEEDBACK Messages](#in-validation-feedback-messages)
```
VALIDATION FEEDBACK $Customer/Email MESSAGE 'Please provide a valid email address';
```
# [Nanoflows vs Microflows](#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](#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 | Per-action `ON ERROR` (no `ErrorEvent`) |
## [When to Use Which](#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-nanoflow-syntax)
```
CREATE [OR MODIFY] NANOFLOW
[FOLDER '']
BEGIN
[]
[]
[RETURN ;]
END;
```
The syntax is identical to `CREATE MICROFLOW` except for the keyword.
## [Supported Activities in Nanoflows](#supported-activities-in-nanoflows)
### [Object Operations](#object-operations-1)
```
-- 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](#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);
-- Call a JavaScript action
$HasNetwork = CALL JAVASCRIPT ACTION NanoflowCommons.HasConnectivity();
```
### [UI Activities](#ui-activities-1)
```
-- Show a page
SHOW PAGE Sales.CartDetail ($Cart = $Cart);
-- Close the current page
CLOSE PAGE;
```
### [Validation](#validation-1)
```
VALIDATION FEEDBACK $Item/Quantity MESSAGE 'Quantity must be at least 1';
```
### [Logging](#logging-1)
```
LOG INFO 'Cart updated with ' + toString($ItemCount) + ' items';
```
### [Control Flow](#control-flow-1)
```
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](#activities-not-available-in-nanoflows)
The following activities are server-only and cannot be used in nanoflows:
- `CALL JAVA ACTION` — Java actions cannot run client-side
- `ErrorEvent` / `RAISE ERROR` — error events are not available in nanoflows
- `DOWNLOAD FILE` — file downloads require server-side processing
- `CALL REST SERVICE` / `SEND REST REQUEST` — REST calls are server-side
- `IMPORT FROM MAPPING` / `EXPORT TO MAPPING` — mapping operations are server-side
- `EXECUTE DATABASE QUERY` — direct SQL requires server
- `TRANSFORM JSON` — JSON transformations are server-side
- `SHOW HOME PAGE` — home page navigation is server-side
- `CALL EXTERNAL ACTION` — external actions are server-side
- All **workflow actions** (call/open workflow, set task outcome, user task, etc.)
> **Note:** Per-action error handling (`on error continue`) IS supported in nanoflows. Only `ErrorEvent` (raise error as a standalone flow action) is forbidden. Note that `on error rollback` is syntactically valid but only rolls back in-memory changes — nanoflows have no database transactions.
## [SHOW and DESCRIBE](#show-and-describe-1)
```
SHOW NANOFLOWS
SHOW NANOFLOWS IN MyModule
DESCRIBE NANOFLOW MyModule.NAV_ShowDetails
```
## [DROP](#drop-1)
```
DROP NANOFLOW MyModule.NAV_ShowDetails;
```
## [Folder Organization](#folder-organization-1)
```
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](#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](#example-page-navigation)
```
CREATE NANOFLOW Sales.NAV_GoToOrderDetail
BEGIN
DECLARE $Order Sales.Order;
SHOW PAGE Sales.Order_Detail ($Order = $Order);
END;
```
## [Security](#security-2)
Nanoflow access control uses GRANT/REVOKE to specify which module roles can execute a nanoflow. See [Grant & Revoke](#grant--revoke) and [Document Access](#microflow-page-and-nanoflow-access) for full syntax and examples.
# [Common Patterns](#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](#crud-patterns)
### [Create and Commit](#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](#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](#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](#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](#validation-pattern-1)
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](#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](#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](#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)](#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](#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)](#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](#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](#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](#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](#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](#validation-checklist)
Before presenting a microflow to the user, verify these rules:
1. Every `DECLARE` has a valid type (`String`, `Integer`, `Boolean`, `Decimal`, `DateTime`, `Module.Entity`, or `List of Module.Entity`)
2. Entity declarations do not use `= empty` (only list declarations do)
3. Every flow path ends with a `RETURN` statement (or the microflow returns void)
4. No empty list variables are used as loop sources
5. No nested loops for list matching
6. All entity and microflow qualified names use the `Module.Name` format
7. `VALIDATION FEEDBACK` uses 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-2)
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](#core-concepts-1)
| 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](#create-page)
The basic syntax for creating a page:
```
CREATE [OR REPLACE] PAGE .
(
[Params: { $Param: Module.Entity | Type [, ...] },]
Title: '',
Layout:
[, Folder: '']
)
{
}
```
### [Minimal Example](#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](#page-with-parameters)
Pages can receive entity objects or primitive values 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](#page-properties)
| Property | Description | Example |
| --- | --- | --- |
| `Params` | Page parameters (entity objects or primitives) | `Params: { $Order: Sales.Order, $Qty: Integer }` |
| `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' }` |
## [Widget Properties](#widget-properties)
### [Responsive Column Widths](#responsive-column-widths)
Layout grid columns support responsive widths for desktop, tablet, and phone:
```
COLUMN col1 (DesktopWidth: 8, TabletWidth: 6, PhoneWidth: 12) { ... }
```
Values are 1-12 (grid units) or `AutoFill`. TabletWidth and PhoneWidth default to auto when omitted.
### [Conditional Visibility](#conditional-visibility)
Any widget can be conditionally visible using an XPath expression in brackets:
```
TEXTBOX txtName (Label: 'Name', Attribute: Name, Visible: [IsActive])
```
Static values also work: `Visible: false` hides the widget unconditionally.
### [Conditional Editability](#conditional-editability)
Input widgets can be conditionally editable:
```
TEXTBOX txtStatus (Label: 'Status', Attribute: Status, Editable: [Status != 'Closed'])
```
Static values: `Editable: Never`, `Editable: Always`.
## [Layouts](#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](#drop-page)
Removes a page from the project:
```
DROP PAGE MyModule.Customer_Edit;
```
## [Inspecting Pages](#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](#see-also-11)
- [Page Structure](#page-structure) – layout selection, content areas, and data sources
- [Widget Types](#widget-types) – full catalog of available widgets
- [Data Binding](#data-binding) – connecting widgets to entity attributes
- [Snippets](#snippets-1) – reusable page fragments
- [ALTER PAGE](#alter-page--alter-snippet) – modifying existing pages in-place
- [Common Patterns](#common-patterns-1) – list page, edit page, master-detail patterns
# [Page Structure](#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-page-syntax)
```
CREATE [OR REPLACE] PAGE .
(
[Params: { $Param: Module.Entity | Type [, ...] },]
Title: '',
Layout:
[, Folder: '']
[, Variables: { $name: Type = 'expression' [, ...] }]
)
{
}
```
## [Layout Reference](#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)
Page parameters define values that must be passed when the page is opened. Parameters use the `$` prefix and can be entity types or primitive types (String, Integer, Decimal, Boolean, DateTime):
```
(
Params: { $Customer: MyModule.Customer },
...
)
```
Multiple parameters are comma-separated, and can mix entity and primitive types:
```
(
Params: { $Order: Sales.Order, $Quantity: Integer, $IsNew: Boolean },
...
)
```
Inside the widget tree, a `DATAVIEW` binds to a parameter using `DataSource: $ParamName`.
## [Page Variables](#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)
Data sources tell a container widget (DataView, DataGrid, ListView, Gallery) where to get its data.
### [Page Parameter Source](#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](#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](#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](#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](#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](#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](#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](#folder-organization-2)
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](#see-also-12)
- [Pages](#pages-2) – page overview and CREATE PAGE basics
- [Widget Types](#widget-types) – full catalog of widgets
- [Data Binding](#data-binding) – attribute binding with the Attribute property
- [Common Patterns](#common-patterns-1) – list page, edit page, master-detail examples
# [Widget Types](#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](#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](#layout-widgets-1)
### [LAYOUTGRID](#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](#row)
A row within a `LAYOUTGRID`. Contains one or more `COLUMN` children.
### [COLUMN](#column)
A column within a `ROW`. Contains any number of child widgets. Column width is determined by the layout grid system.
### [CONTAINER](#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](#customcontainer)
Similar to `CONTAINER` but used for custom-styled containers.
## [Data Widgets](#data-widgets)
### [DATAVIEW](#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 parameter
- `DataSource: MICROFLOW Module.MF_Name` – microflow returning a single object
- `DataSource: NANOFLOW Module.NF_Name` – nanoflow returning a single object
- `DataSource: SELECTION widgetName` – currently selected item from a list widget
- `DataSource: ASSOCIATION AssocName` – follow an association from a parent context
### [DATAGRID](#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](#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](#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)
}
}
```
**Responsive column properties** control how many columns the grid uses per breakpoint:
| Property | Description | Default |
| --- | --- | --- |
| `DesktopColumns` | Number of columns on desktop | `1` |
| `TabletColumns` | Number of columns on tablet | `1` |
| `PhoneColumns` | Number of columns on phone | `1` |
```
GALLERY galBoard (
DataSource: DATABASE MyModule.Cell,
DesktopColumns: 9,
TabletColumns: 4,
PhoneColumns: 2
) {
DYNAMICTEXT txtVal (Content: '{1}', Attribute: Value)
}
```
## [Input Widgets](#input-widgets-1)
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](#textbox)
Single-line text input. The most common input widget:
```
TEXTBOX txtName (Label: 'Name', Attribute: Name)
TEXTBOX txtEmail (Label: 'Email', Attribute: Email)
```
### [TEXTAREA](#textarea)
Multi-line text input for longer text:
```
TEXTAREA txtDescription (Label: 'Description', Attribute: Description)
```
### [CHECKBOX](#checkbox)
Boolean (true/false) input:
```
CHECKBOX cbActive (Label: 'Active', Attribute: IsActive)
```
### [RADIOBUTTONS](#radiobuttons)
Displays enumeration or boolean values as radio buttons:
```
RADIOBUTTONS rbStatus (Label: 'Status', Attribute: Status)
```
### [DATEPICKER](#datepicker)
Date and/or time input:
```
DATEPICKER dpBirthDate (Label: 'Birth Date', Attribute: BirthDate)
```
### [COMBOBOX](#combobox)
Dropdown selection for enumeration values or associations. Uses the pluggable ComboBox widget:
```
COMBOBOX cbStatus (Label: 'Status', Attribute: Status)
```
### [REFERENCESELECTOR](#referenceselector)
Dropdown for selecting an associated object via a reference association:
```
REFERENCESELECTOR rsCategory (Label: 'Category', Attribute: Category)
```
## [Display Widgets](#display-widgets-1)
### [DYNAMICTEXT](#dynamictext)
Displays dynamic text content, often with attribute values:
```
DYNAMICTEXT txtGreeting (Content: 'Welcome, {1}', Attribute: Name)
```
### [IMAGE / STATICIMAGE / DYNAMICIMAGE](#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](#action-widgets)
### [ACTIONBUTTON](#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](#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](#structure-widgets)
### [HEADER](#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)
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](#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](#snippetcall)
Embeds a snippet (reusable page fragment) into the page:
```
SNIPPETCALL scNav (Snippet: MyModule.NavigationMenu)
```
## [Navigation Widgets](#navigation-widgets)
### [NAVIGATIONLIST](#navigationlist)
Renders a navigation list with clickable items:
```
NAVIGATIONLIST navMain {
-- navigation items
}
```
## [Common Widget Properties](#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](#see-also-13)
- [Pages](#pages-2) – page overview and CREATE PAGE basics
- [Page Structure](#page-structure) – layout selection and data sources
- [Data Binding](#data-binding) – connecting widgets to attributes
- [ALTER PAGE](#alter-page--alter-snippet) – modifying widgets in existing pages
# [Data Binding](#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](#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-1)
Data sources determine where a container widget gets its data. Different widget types support different data source types.
### [Page Parameter Source](#page-parameter-source-1)
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](#database-source-1)
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](#microflow-source-1)
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](#nanoflow-source-1)
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](#association-source-1)
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](#selection-source-1)
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](#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](#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](#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](#see-also-14)
- [Pages](#pages-2) – page overview
- [Page Structure](#page-structure) – data source types in detail
- [Widget Types](#widget-types) – available widgets and their properties
- [Common Patterns](#common-patterns-1) – master-detail and other binding patterns
# [Snippets](#snippets-1)
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-snippet)
```
CREATE [OR REPLACE] SNIPPET .
(
[Params: { $Param: Module.Entity [, ...] },]
[Folder: '']
)
{
}
```
### [Basic Snippet](#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](#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](#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](#inspecting-snippets)
```
-- List all snippets in a module
SHOW SNIPPETS IN MyModule;
-- View full MDL definition
DESCRIBE SNIPPET MyModule.CustomerCard;
```
## [DROP SNIPPET](#drop-snippet)
```
DROP SNIPPET MyModule.Footer;
```
## [ALTER SNIPPET](#alter-snippet)
Snippets support the same in-place modification operations as pages. See [ALTER PAGE / ALTER SNIPPET](#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](#see-also-15)
- [Pages](#pages-2) – page overview
- [Widget Types](#widget-types) – available widgets including SNIPPETCALL
- [ALTER PAGE](#alter-page--alter-snippet) – modifying snippets and pages in-place
- [Common Patterns](#common-patterns-1) – patterns that use snippets
# [ALTER PAGE / ALTER SNIPPET](#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](#syntax-3)
```
ALTER PAGE . {
};
ALTER SNIPPET . {
};
```
## [Operations](#operations)
### [SET – Modify Widget Properties](#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](#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](#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
};
```
### [SET Layout – Change Page Layout](#set-layout--change-page-layout)
Switch a page’s layout without rebuilding the widget tree. All widget content is preserved – only the layout reference and placeholder mappings are updated.
```
-- Auto-map placeholders by name (common case)
ALTER PAGE Module.EditPage {
SET Layout = Atlas_Core.Atlas_Default
};
-- Explicit mapping when placeholder names differ
ALTER PAGE Module.EditPage {
SET Layout = Atlas_Core.Atlas_SideBar MAP (Main AS Content, Extra AS Sidebar)
};
```
When both the old and new layouts share the same placeholder names (e.g., both have `Main`), no `MAP` clause is needed – placeholders are matched automatically. Use `MAP` when the new layout has different placeholder names.
Not supported for snippets (snippets don’t have layouts).
### [INSERT – Add Widgets](#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](#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--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](#page-variables-1)
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](#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](#workflow-tips)
1. **Discover widget names first** – Run `DESCRIBE PAGE Module.PageName` to see the current widget tree with all widget names.
2. **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).
3. **Use CREATE OR REPLACE for major rewrites** – When restructuring the entire page layout, a full replacement is cleaner.
## [Examples](#examples-2)
### [Add a Field to an Edit Page](#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](#change-button-behavior)
```
ALTER PAGE MyModule.Order_Edit {
SET (Caption = 'Submit Order', ButtonStyle = Success) ON btnSave;
SET Caption = 'Discard' ON btnCancel
};
```
### [DataGrid Column Operations](#datagrid-column-operations)
DataGrid2 columns are addressable using dotted notation: `gridName.columnName`. Use `DESCRIBE PAGE` to discover column names (derived from the attribute short name or caption).
```
-- Add a column after an existing one
ALTER PAGE MyModule.Customer_Overview {
INSERT AFTER dgCustomers.Email {
COLUMN Phone (Attribute: Phone, Caption: 'Phone')
}
};
-- Remove a column
ALTER PAGE MyModule.Customer_Overview {
DROP WIDGET dgCustomers.OldColumn
};
-- Change a column's caption
ALTER PAGE MyModule.Customer_Overview {
SET Caption = 'E-mail Address' ON dgCustomers.Email
};
-- Replace a column
ALTER PAGE MyModule.Customer_Overview {
REPLACE dgCustomers.Notes WITH {
COLUMN Description (Attribute: Description, Caption: 'Description')
}
};
```
## [See Also](#see-also-16)
- [Pages](#pages-2) – page overview and CREATE PAGE basics
- [Widget Types](#widget-types) – widgets available for INSERT and REPLACE
- [Snippets](#snippets-1) – ALTER SNIPPET uses the same operations
- [Common Patterns](#common-patterns-1) – page layout patterns
# [Common Patterns](#common-patterns-1)
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](#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)](#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)](#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](#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](#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](#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](#crud-page-set)
A complete set of pages for managing an entity typically includes:
1. **Overview page** – DataGrid with list of records + New/Edit/Delete buttons
2. **Edit page (popup)** – DataView with input fields + Save/Cancel
3. **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](#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](#see-also-17)
- [Pages](#pages-2) – page overview
- [Page Structure](#page-structure) – data sources and layout references
- [Widget Types](#widget-types) – available widgets
- [ALTER PAGE](#alter-page--alter-snippet) – incremental modifications to existing pages
- [Snippets](#snippets-1) – reusable page fragments
# [Security](#security-3)
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](#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-architecture)
Security in Mendix is organized in layers:
1. **Module Roles** – defined per module, they represent permissions within that module (e.g., `Shop.Admin`, `Shop.Viewer`)
2. **User Roles** – project-level roles that aggregate module roles across modules (e.g., `AppAdmin` combines `Shop.Admin` and `System.Administrator`)
3. **Entity Access** – CRUD permissions and XPath constraints per entity per module role
4. **Document Access** – execute/view permissions on microflows, nanoflows, and pages per module role
5. **Demo Users** – test accounts with assigned user roles for development
## [Inspecting Security](#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](#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](#see-also-18)
- [Module Roles and User Roles](#module-roles-and-user-roles) – defining roles
- [Entity Access](#entity-access-1) – CRUD permissions per entity
- [Document Access](#microflow-page-and-nanoflow-access) – microflow, page, and nanoflow permissions
- [GRANT / REVOKE](#grant--revoke) – granting and revoking permissions
- [Demo Users](#demo-users-1) – creating test accounts
# [Module Roles and User Roles](#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](#module-roles-1)
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)
```
CREATE MODULE ROLE . [DESCRIPTION ''];
```
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)
```
DROP MODULE ROLE Shop.Viewer;
```
### [Listing Module Roles](#listing-module-roles)
```
SHOW MODULE ROLES;
SHOW MODULE ROLES IN Shop;
```
## [User Roles](#user-roles-1)
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)
```
CREATE USER 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](#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)
```
DROP USER ROLE AppViewer;
```
### [Listing User Roles](#listing-user-roles)
```
SHOW USER ROLES;
```
## [Typical Setup](#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](#see-also-19)
- [Security](#security-3) – overview of the security model
- [Entity Access](#entity-access-1) – granting CRUD permissions to module roles
- [Document Access](#microflow-page-and-nanoflow-access) – granting microflow and page access
- [GRANT / REVOKE](#grant--revoke) – the GRANT and REVOKE statements
- [Demo Users](#demo-users-1) – creating test accounts with user roles
# [Entity Access](#entity-access-1)
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-on-entities)
```
GRANT . ON . () [WHERE ''];
```
The `` 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 (, ...)` | Read access to listed members only |
| Write all | `WRITE *` | Write access to all attributes and associations |
| Write specific | `WRITE (, ...)` | Write access to listed members only |
## [Examples](#examples-3)
### [Full Access](#full-access)
Grant all operations on all members:
```
GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);
```
### [Read-Only Access](#read-only-access)
```
GRANT Shop.Viewer ON Shop.Customer (READ *);
```
### [Selective Member Access](#selective-member-access)
Restrict read and write to specific attributes:
```
GRANT Shop.User ON Shop.Customer (READ (Name, Email, Status), WRITE (Email));
```
### [XPath Constraints](#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.
### [Additive Behavior](#additive-behavior)
GRANT is **additive**. If a role already has an access rule on the entity, the new rights are merged in without removing existing permissions:
```
-- Initial grant
GRANT Shop.User ON Shop.Customer (READ (Name, Email));
-- Add Notes access — Name and Email are preserved
GRANT Shop.User ON Shop.Customer (READ (Notes));
-- Result: READ (Name, Email, Notes)
-- Upgrade Email to writable — existing reads preserved
GRANT Shop.User ON Shop.Customer (WRITE (Email));
-- Result: READ (Name, Notes), WRITE (Email)
```
### [Multiple Roles on the Same Entity](#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](#revoke-on-entities)
Remove an entity access rule entirely or revoke specific rights:
```
-- Full revoke (removes entire rule)
REVOKE . ON .;
-- Partial revoke (downgrades specific rights)
REVOKE . ON . ();
```
Examples:
```
-- Remove all access for Viewer
REVOKE Shop.Viewer ON Shop.Customer;
-- Remove read access on a specific attribute
REVOKE Shop.User ON Shop.Customer (READ (Notes));
-- Downgrade write to read-only on Email
REVOKE Shop.User ON Shop.Customer (WRITE (Email));
```
A full `REVOKE` (without rights list) removes the entire access rule. A partial `REVOKE` downgrades specific rights: `REVOKE READ (x)` sets member x to no access, `REVOKE WRITE (x)` downgrades from ReadWrite to ReadOnly.
## [Viewing Entity Access](#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](#see-also-20)
- [Security](#security-3) – overview of the security model
- [Module Roles and User Roles](#module-roles-and-user-roles) – defining the roles referenced in access rules
- [Document Access](#microflow-page-and-nanoflow-access) – microflow and page access
- [GRANT / REVOKE](#grant--revoke) – complete GRANT and REVOKE reference
# [Microflow, Page, and Nanoflow Access](#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](#microflow-access)
### [GRANT EXECUTE ON MICROFLOW](#grant-execute-on-microflow)
```
GRANT EXECUTE ON MICROFLOW . TO . [, ...];
```
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)
```
REVOKE EXECUTE ON MICROFLOW . FROM . [, ...];
```
Example:
```
REVOKE EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder FROM Shop.User;
```
## [Page Access](#page-access)
### [GRANT VIEW ON PAGE](#grant-view-on-page)
```
GRANT VIEW ON PAGE . TO . [, ...];
```
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)
```
REVOKE VIEW ON PAGE . FROM . [, ...];
```
Example:
```
REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;
```
## [Nanoflow Access](#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](#viewing-document-access)
```
SHOW ACCESS ON MICROFLOW Shop.ACT_ProcessOrder;
SHOW ACCESS ON PAGE Shop.Order_Overview;
```
## [Typical Pattern](#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](#see-also-21)
- [Security](#security-3) – overview of the security model
- [Entity Access](#entity-access-1) – CRUD permissions on entities
- [GRANT / REVOKE](#grant--revoke) – complete GRANT and REVOKE reference
- [Module Roles and User Roles](#module-roles-and-user-roles) – defining the roles
# [GRANT / REVOKE](#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](#entity-access-2)
### [GRANT](#grant)
```
GRANT . ON . () [WHERE ''];
```
Where `` is a comma-separated list of:
| Right | Description |
| --- | --- |
| `CREATE` | Allow creating new objects |
| `DELETE` | Allow deleting objects |
| `READ *` | Read all members |
| `READ (, ...)` | Read specific members only |
| `WRITE *` | Write all members |
| `WRITE (, ...)` | Write specific members only |
GRANT is **additive**: if the role already has an access rule on the entity, new rights are merged in. Existing permissions are never removed by a GRANT — only upgraded.
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'']';
-- Additive: adds Notes to existing read access without removing Name, Email
GRANT Shop.User ON Shop.Customer (READ (Notes));
```
### [REVOKE](#revoke)
Remove an entity access rule entirely, or revoke specific rights:
```
-- Full revoke (removes entire rule)
REVOKE . ON .;
-- Partial revoke (downgrades specific rights)
REVOKE . ON . ();
```
For partial revoke, `REVOKE READ (x)` sets member x access to None. `REVOKE WRITE (x)` downgrades member x from ReadWrite to ReadOnly. `REVOKE CREATE` / `REVOKE DELETE` removes the structural permission.
Examples:
```
-- Remove all access
REVOKE Shop.Viewer ON Shop.Customer;
-- Remove read access on a specific member
REVOKE Shop.User ON Shop.Customer (READ (Notes));
-- Downgrade write to read-only
REVOKE Shop.User ON Shop.Customer (WRITE (Email));
-- Remove delete permission only
REVOKE Shop.User ON Shop.Customer (DELETE);
```
## [Microflow Access](#microflow-access-1)
### [GRANT EXECUTE ON MICROFLOW](#grant-execute-on-microflow-1)
```
GRANT EXECUTE ON MICROFLOW . TO . [, ...];
```
Example:
```
GRANT EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder TO Shop.User, Shop.Admin;
```
### [REVOKE EXECUTE ON MICROFLOW](#revoke-execute-on-microflow-1)
```
REVOKE EXECUTE ON MICROFLOW . FROM . [, ...];
```
Example:
```
REVOKE EXECUTE ON MICROFLOW Shop.ACT_ProcessOrder FROM Shop.User;
```
## [Page Access](#page-access-1)
### [GRANT VIEW ON PAGE](#grant-view-on-page-1)
```
GRANT VIEW ON PAGE . TO . [, ...];
```
Example:
```
GRANT VIEW ON PAGE Shop.Order_Overview TO Shop.User, Shop.Admin;
```
### [REVOKE VIEW ON PAGE](#revoke-view-on-page-1)
```
REVOKE VIEW ON PAGE . FROM . [, ...];
```
Example:
```
REVOKE VIEW ON PAGE Shop.Admin_Dashboard FROM Shop.User;
```
## [Nanoflow Access](#nanoflow-access-1)
```
GRANT EXECUTE ON NANOFLOW . TO . [, ...];
REVOKE EXECUTE ON NANOFLOW . FROM . [, ...];
```
## [Workflow Access](#workflow-access)
> **Not supported.** Mendix workflows do not have document-level `AllowedModuleRoles` (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting.
## [OData Service Access](#odata-service-access)
```
GRANT ACCESS ON ODATA SERVICE . TO . [, ...];
REVOKE ACCESS ON ODATA SERVICE . FROM . [, ...];
```
## [Complete Example](#complete-example-3)
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](#see-also-22)
- [Security](#security-3) – overview of the security model
- [Entity Access](#entity-access-1) – details on entity CRUD permissions and XPath constraints
- [Document Access](#microflow-page-and-nanoflow-access) – microflow, page, and nanoflow access patterns
- [Module Roles and User Roles](#module-roles-and-user-roles) – creating and managing roles
- [Demo Users](#demo-users-1) – creating test accounts
# [Demo Users](#demo-users-1)
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)
```
CREATE DEMO USER '' PASSWORD ''
[ENTITY .]
( [, ...]);
```
| Parameter | Description |
| --- | --- |
| `` | Login name for the demo user |
| `` | 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. |
| `` | One or more project-level user roles to assign |
### [Examples](#examples-4)
```
-- 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)
```
DROP DEMO USER '';
```
Example:
```
DROP DEMO USER 'demo_admin';
```
## [Enabling Demo Users](#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](#listing-demo-users)
```
SHOW DEMO USERS;
```
## [Typical Setup](#typical-setup-1)
```
-- 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](#see-also-23)
- [Security](#security-3) – overview of the security model
- [Module Roles and User Roles](#module-roles-and-user-roles) – defining the user roles assigned to demo users
- [GRANT / REVOKE](#grant--revoke) – complete permission management reference
# [Navigation and Settings](#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](#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](#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](#navigation-profiles)
| Profile | Description |
| --- | --- |
| `Responsive` | Web browser (desktop, laptop) |
| `Tablet` | Tablet browser |
| `Phone` | Phone browser |
| `NativePhone` | Native mobile (React Native) |
See [Navigation Profiles](#navigation-profiles-1) for full syntax.
## [Settings Overview](#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](#project-settings) for the ALTER SETTINGS syntax.
## [See Also](#see-also-24)
- [Navigation Profiles](#navigation-profiles-1) – creating and replacing navigation profiles
- [Home Pages and Menus](#home-pages-and-menus) – home page assignments and menu structures
- [Project Settings](#project-settings) – runtime, configuration, and language settings
# [Navigation Profiles](#navigation-profiles-1)
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-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](#create-or-replace-navigation)
Replaces an entire navigation profile:
```
CREATE OR REPLACE NAVIGATION
HOME PAGE .
[HOME PAGE . FOR .]
[LOGIN PAGE .]
[NOT FOUND PAGE .]
[MENU (
)]
```
### [Full Example](#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](#minimal-example-1)
A profile only requires a home page:
```
CREATE OR REPLACE NAVIGATION Phone
HOME PAGE MyModule.Home_Phone;
```
## [DESCRIBE NAVIGATION](#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](#see-also-25)
- [Navigation and Settings](#navigation-and-settings) – overview of navigation and settings
- [Home Pages and Menus](#home-pages-and-menus) – details on home page and menu configuration
- [Project Settings](#project-settings) – runtime and configuration settings
# [Home Pages and Menus](#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](#home-pages)
### [Default Home Page](#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](#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](#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](#not-found-page)
An optional custom 404 page:
```
CREATE OR REPLACE NAVIGATION Responsive
HOME PAGE MyModule.Home_Web
NOT FOUND PAGE MyModule.Custom404;
```
## [Menus](#menus)
The `MENU` block defines the navigation menu as a tree of items and submenus.
### [Menu Items](#menu-items)
A menu item links a label to a page:
```
MENU ITEM '' PAGE .;
```
### [Submenus](#submenus)
Nest items inside a `MENU '' (...)` block:
```
MENU '' (
);
```
### [Complete Example](#complete-example-4)
```
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](#inspecting-menus)
```
-- View menu tree
SHOW NAVIGATION MENU;
SHOW NAVIGATION MENU Responsive;
-- View home page assignments
SHOW NAVIGATION HOMES;
```
## [See Also](#see-also-26)
- [Navigation and Settings](#navigation-and-settings) – overview of navigation concepts
- [Navigation Profiles](#navigation-profiles-1) – profile types and CREATE OR REPLACE syntax
- [Project Settings](#project-settings) – runtime and configuration settings
# [Project 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](#inspecting-settings)
```
-- Overview of all settings
SHOW SETTINGS;
-- Full MDL output (round-trippable)
DESCRIBE SETTINGS;
```
## [ALTER SETTINGS](#alter-settings)
Settings are organized into categories. Each `ALTER SETTINGS` command targets one category.
### [Model Settings](#model-settings)
Runtime-level settings such as the after-startup microflow, hashing algorithm, and Java version:
```
ALTER SETTINGS MODEL = ;
```
Examples:
```
ALTER SETTINGS MODEL AfterStartupMicroflow = 'MyModule.ACT_Startup';
ALTER SETTINGS MODEL HashAlgorithm = 'BCrypt';
ALTER SETTINGS MODEL JavaVersion = '17';
```
### [Configuration Settings](#configuration-settings)
Server configuration settings like database type, URL, and HTTP port. Each configuration is identified by name (commonly `'default'`):
```
ALTER SETTINGS CONFIGURATION '' = ;
```
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](#constant-overrides)
Override a constant value within a specific configuration:
```
ALTER SETTINGS CONSTANT '' VALUE '' IN CONFIGURATION '';
```
Example:
```
ALTER SETTINGS CONSTANT 'MyModule.ApiBaseUrl' VALUE 'https://staging.example.com' IN CONFIGURATION 'default';
```
### [Language Settings](#language-settings)
Set the default language for the application:
```
ALTER SETTINGS LANGUAGE = ;
```
Example:
```
ALTER SETTINGS LANGUAGE DefaultLanguageCode = 'en_US';
```
### [Workflow Settings](#workflow-settings)
Configure workflow behavior such as the user entity and task parallelism:
```
ALTER SETTINGS WORKFLOWS = ;
```
Examples:
```
ALTER SETTINGS WORKFLOWS UserEntity = 'Administration.Account';
ALTER SETTINGS WORKFLOWS DefaultTaskParallelism = '5';
```
## [See Also](#see-also-27)
- [Navigation and Settings](#navigation-and-settings) – overview of navigation and settings
- [Navigation Profiles](#navigation-profiles-1) – navigation profile configuration
- [Workflows](#workflows-1) – workflow definitions
# [Workflows](#workflows-1)
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](#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](#microflows-and-nanoflows-1) instead. See [Workflow vs Microflow](#workflow-vs-microflow) for a detailed comparison.
## [Inspecting Workflows](#inspecting-workflows)
```
-- List all workflows
SHOW WORKFLOWS;
SHOW WORKFLOWS IN MyModule;
-- View full definition
DESCRIBE WORKFLOW MyModule.ApprovalFlow;
```
## [Quick Example](#quick-example-2)
```
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](#see-also-28)
- [Workflow Structure](#workflow-structure) – full CREATE WORKFLOW syntax
- [Activity Types](#activity-types-1) – all workflow activity types
- [Workflow vs Microflow](#workflow-vs-microflow) – choosing between the two
- [GRANT / REVOKE](#grant--revoke) – workflow access is controlled through triggering microflows and UserTask targeting, not document-level roles
# [Workflow Structure](#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-workflow)
```
CREATE [OR MODIFY] WORKFLOW .
PARAMETER $: .
[OVERVIEW PAGE .]
BEGIN
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](#full-example-1)
```
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)
```
DROP WORKFLOW HR.OnboardEmployee;
```
## [Workflow Access](#workflow-access-1)
> **Not supported.** Mendix workflows do not have document-level `AllowedModuleRoles` (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting.
## [See Also](#see-also-29)
- [Workflows](#workflows-1) – overview and when to use workflows
- [Activity Types](#activity-types-1) – details on each activity type
- [Workflow vs Microflow](#workflow-vs-microflow) – choosing the right construct
# [Activity Types](#activity-types-1)
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](#user-task)
A user task pauses the workflow until a user completes it. Each outcome resumes the workflow down a different path.
```
USER TASK ''
[PAGE .]
[TARGETING MICROFLOW .]
OUTCOMES '' { } ['' { }] ...;
```
| Element | Description |
| --- | --- |
| `` | Internal activity name |
| `` | 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](#call-microflow-1)
Execute a microflow as part of the workflow. Optionally specify a comment and outcomes:
```
CALL MICROFLOW